tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.moreDebug = False 300 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 301 302 self.historyFile = None 303 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 304 305 See also: `History()`. 306 """ 307 308 self.htmlHistoryFile = "index.html" 309 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 310 311 See also: `ShowHistoryChart()`. 312 """ 313 314 self.instrumentsFile = "instruments.md" 315 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 316 317 See also: `ShowInstrumentsInfo()`. 318 """ 319 320 self.searchResultsFile = "search-results.md" 321 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 322 323 See also: `SearchInstruments()`. 324 """ 325 326 self.pricesFile = "prices.md" 327 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 328 329 See also: `GetListOfPrices()`. 330 """ 331 332 self.infoFile = "info.md" 333 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 334 335 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 336 """ 337 338 self.bondsXLSXFile = "ext-bonds.xlsx" 339 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 340 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 341 342 See also: `ExtendBondsData()`. 343 """ 344 345 self.calendarFile = "calendar.md" 346 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 347 348 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 349 350 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 351 """ 352 353 self.overviewFile = "overview.md" 354 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 355 356 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 357 """ 358 359 self.overviewDigestFile = "overview-digest.md" 360 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 361 362 See also: `Overview()` with parameter `details="digest"`. 363 """ 364 365 self.overviewPositionsFile = "overview-positions.md" 366 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 367 368 See also: `Overview()` with parameter `details="positions"`. 369 """ 370 371 self.overviewOrdersFile = "overview-orders.md" 372 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 373 374 See also: `Overview()` with parameter `details="orders"`. 375 """ 376 377 self.overviewAnalyticsFile = "overview-analytics.md" 378 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 379 380 See also: `Overview()` with parameter `details="analytics"`. 381 """ 382 383 self.overviewBondsCalendarFile = "overview-calendar.md" 384 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 385 386 See also: `Overview()` with parameter `details="calendar"`. 387 """ 388 389 self.reportFile = "deals.md" 390 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 391 392 See also: `Deals()`. 393 """ 394 395 self.withdrawalLimitsFile = "limits.md" 396 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 397 398 See also: `OverviewLimits()` and `RequestLimits()`. 399 """ 400 401 self.userInfoFile = "user-info.md" 402 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 403 404 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 405 """ 406 407 self.userAccountsFile = "accounts.md" 408 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 409 410 See also: `OverviewAccounts()`, `RequestAccounts()`. 411 """ 412 413 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 414 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 415 416 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 417 418 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 419 """ 420 421 self.iList = None # init iList for raw instruments data 422 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 423 424 See also: `Listing()`, `DumpInstruments()`. 425 """ 426 427 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 428 if useCache: 429 if os.path.exists(self.iListDumpFile): 430 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 431 curTime = datetime.now(tzutc()) 432 433 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 434 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 437 438 else: 439 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 440 441 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 442 os.path.abspath(self.iListDumpFile), 443 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 444 )) 445 446 else: 447 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 448 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 449 450 else: 451 self.iList = self.Listing() # request new raw instruments data from broker server 452 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 453 454 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 455 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 456 457 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 458 """ 459 460 def _ParseJSON(self, rawData="{}") -> dict: 461 """ 462 Parse JSON from response string. 463 464 :param rawData: this is a string with JSON-formatted text. 465 :return: JSON (dictionary), parsed from server response string. 466 """ 467 responseJSON = json.loads(rawData) if rawData else {} 468 469 if self.moreDebug: 470 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 471 472 return responseJSON 473 474 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 475 """ 476 Send GET or POST request to broker server and receive JSON object. 477 478 self.header: must be defining with dictionary of headers. 479 self.body: if define then used as request body. None by default. 480 self.timeout: global request timeout, 15 seconds by default. 481 :param url: url with REST request. 482 :param reqType: send "GET" or "POST" request. "GET" by default. 483 :param retry: how many times retry after first request if an 5xx server errors occurred. 484 :param pause: sleep time in seconds between retries. 485 :return: response JSON (dictionary) from broker. 486 """ 487 if reqType not in ("GET", "POST"): 488 uLogger.error("You can define request type: 'GET' or 'POST'!") 489 raise Exception("Incorrect value") 490 491 if self.moreDebug: 492 uLogger.debug("Request parameters:") 493 uLogger.debug(" - REST API URL: {}".format(url)) 494 uLogger.debug(" - request type: {}".format(reqType)) 495 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 496 uLogger.debug(" - body:\n{}".format(self.body)) 497 498 # fast hack to avoid all operations with some tickers/FIGI 499 responseJSON = {} 500 oK = True 501 for item in self.exclude: 502 if item in url: 503 if self.moreDebug: 504 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 505 506 oK = False 507 break 508 509 if oK: 510 counter = 0 511 response = None 512 errMsg = "" 513 514 while not response and counter <= retry: 515 if reqType == "GET": 516 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 517 518 if reqType == "POST": 519 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 520 521 if self.moreDebug: 522 uLogger.debug("Response:") 523 uLogger.debug(" - status code: {}".format(response.status_code)) 524 uLogger.debug(" - reason: {}".format(response.reason)) 525 uLogger.debug(" - body length: {}".format(len(response.text))) 526 uLogger.debug(" - headers:\n{}".format(response.headers)) 527 528 # Server returns some headers: 529 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 530 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 531 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 532 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 533 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 534 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 535 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 536 sleep(rateLimitWait) 537 538 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 539 if 400 <= response.status_code < 500: 540 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 541 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 542 counter = retry + 1 543 544 if 500 <= response.status_code < 600: 545 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 546 uLogger.debug(" - not oK, {}".format(errMsg)) 547 counter += 1 548 549 if counter <= retry: 550 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 551 sleep(pause) 552 553 responseJSON = self._ParseJSON(rawData=response.text) 554 555 if errMsg: 556 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 557 uLogger.error(" - not oK, {}".format(errMsg)) 558 559 return responseJSON 560 561 def _IUpdater(self, iType: str) -> tuple: 562 """ 563 Request instrument by type from server. See available API methods for instruments: 564 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 565 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 566 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 567 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 568 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 569 570 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 571 :return: tuple with iType name and list of available instruments of current type for defined user token. 572 """ 573 result = [] 574 575 if iType in TKS_INSTRUMENTS: 576 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 577 578 # all instruments have the same body in API v2 requests: 579 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 580 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 581 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 582 583 return iType, result 584 585 def _IWrapper(self, kwargs): 586 """ 587 Wrapper runs instrument's update method `_IUpdater()`. 588 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 589 """ 590 return self._IUpdater(**kwargs) 591 592 def Listing(self) -> dict: 593 """ 594 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 595 596 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 597 """ 598 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 599 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 600 601 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 602 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 603 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 604 605 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 606 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 607 poolUpdater.close() 608 609 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 610 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 611 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 612 613 # calculate minimum price increment (step) for all instruments and set up instrument's type: 614 for iType in iList.keys(): 615 for ticker in iList[iType]: 616 iList[iType][ticker]["type"] = iType 617 618 if "minPriceIncrement" in iList[iType][ticker].keys(): 619 iList[iType][ticker]["step"] = NanoToFloat( 620 iList[iType][ticker]["minPriceIncrement"]["units"], 621 iList[iType][ticker]["minPriceIncrement"]["nano"], 622 ) 623 624 else: 625 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 626 627 return iList 628 629 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 630 """ 631 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 632 633 See also: `DumpInstruments()`, `Listing()`. 634 635 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 636 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 637 """ 638 if self.iListDumpFile is None or not self.iListDumpFile: 639 uLogger.error("Output name of dump file must be defined!") 640 raise Exception("Filename required") 641 642 if not self.iList or forceUpdate: 643 self.iList = self.Listing() 644 645 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 646 647 # Save as XLSX with separated sheets for every type of instruments: 648 with pd.ExcelWriter( 649 path=xlsxDumpFile, 650 date_format=TKS_DATE_FORMAT, 651 datetime_format=TKS_DATE_TIME_FORMAT, 652 mode="w", 653 ) as writer: 654 for iType in TKS_INSTRUMENTS: 655 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 656 df = df[sorted(df)] # sorted by column names 657 df = df.applymap( 658 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 659 na_action="ignore", 660 ) # converting numbers from nano-type to float in every cell 661 df.to_excel( 662 writer, 663 sheet_name=iType, 664 encoding="UTF-8", 665 freeze_panes=(1, 1), 666 ) # saving as XLSX-file with freeze first row and column as headers 667 668 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 669 670 def DumpInstruments(self, forceUpdate: bool = True) -> str: 671 """ 672 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 673 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 674 675 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 676 677 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 678 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 679 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 680 """ 681 if self.iListDumpFile is None or not self.iListDumpFile: 682 uLogger.error("Output name of dump file must be defined!") 683 raise Exception("Filename required") 684 685 if not self.iList or forceUpdate: 686 self.iList = self.Listing() 687 688 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 689 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 690 fH.write(jsonDump) 691 692 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 693 694 return jsonDump 695 696 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 697 """ 698 Show information about one instrument defined by json data and prints it in Markdown format. 699 700 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 701 702 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 703 :param show: if `True` then also printing information about instrument and its current price. 704 :return: multilines text in Markdown format with information about one instrument. 705 """ 706 splitLine = "| | |\n" 707 infoText = "" 708 709 if iJSON is not None and iJSON and isinstance(iJSON, dict): 710 info = [ 711 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 712 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 713 "| Parameters | Values |\n", 714 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 715 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 716 "| Full name: | {:<54} |\n".format(iJSON["name"]), 717 ] 718 719 if "sector" in iJSON.keys() and iJSON["sector"]: 720 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 721 722 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 723 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 724 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 725 ))) 726 727 info.extend([ 728 splitLine, 729 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 730 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 731 ]) 732 733 if "isin" in iJSON.keys() and iJSON["isin"]: 734 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 735 736 if "classCode" in iJSON.keys(): 737 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 738 739 info.extend([ 740 splitLine, 741 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 742 splitLine, 743 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 744 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 745 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 746 ]) 747 748 if iJSON["figi"]: 749 self.figi = iJSON["figi"] 750 iJSON = iJSON | self.RequestTradingStatus() 751 752 info.extend([ 753 splitLine, 754 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 755 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 756 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 757 ]) 758 759 info.append(splitLine) 760 761 if "type" in iJSON.keys() and iJSON["type"]: 762 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 763 764 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 765 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 766 767 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 768 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 769 770 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 771 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 772 773 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 774 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 775 776 if "focusType" in iJSON.keys() and iJSON["focusType"]: 777 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 778 779 if "assetType" in iJSON.keys() and iJSON["assetType"]: 780 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 781 782 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 783 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 784 785 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 786 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 787 788 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 789 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 790 791 if "currency" in iJSON.keys(): 792 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 793 794 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 795 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 796 797 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 798 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 799 800 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 801 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 802 803 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 804 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 805 806 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 807 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 808 809 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 810 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 811 812 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 813 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 814 815 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 816 info.append("| Perpetual bond: | Yes |\n") 817 818 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 819 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 820 821 iExt = None 822 if iJSON["type"] == "Bonds": 823 info.extend([ 824 splitLine, 825 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 826 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 827 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 828 iJSON["nominal"]["currency"], 829 )), 830 ]) 831 832 if "floatingCouponFlag" in iJSON.keys(): 833 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 834 835 if "amortizationFlag" in iJSON.keys(): 836 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 837 838 info.append(splitLine) 839 840 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 841 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 842 843 if iJSON["figi"]: 844 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 845 846 info.extend([ 847 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 848 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 849 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 850 ]) 851 852 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 853 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 854 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 855 iJSON["aciValue"]["currency"] 856 ))) 857 858 if "currentPrice" in iJSON.keys(): 859 info.append(splitLine) 860 861 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 862 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 863 864 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 865 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 866 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 867 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 868 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 869 870 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 871 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 872 873 info.extend([ 874 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 875 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 876 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 877 )), 878 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 879 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 880 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 881 )), 882 "| Changes between last deal price and last close | {:<54} |\n".format( 883 "{:.2f}%{}".format( 884 iJSON["currentPrice"]["changes"], 885 " ({}{:.2f} {})".format( 886 "+" if bondChangesDelta > 0 else "", 887 bondChangesDelta, 888 aciCurrency 889 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 890 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 891 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 892 currency 893 ), 894 ) 895 ), 896 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 897 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 900 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 901 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 902 )), 903 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 904 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 906 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 907 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 908 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 909 )), 910 ]) 911 912 if "lot" in iJSON.keys(): 913 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 914 915 if "step" in iJSON.keys() and iJSON["step"] != 0: 916 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 917 918 # Add bond payment calendar: 919 if iJSON["type"] == "Bonds": 920 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 921 info.extend(["\n", strCalendar]) 922 923 infoText += "".join(info) 924 925 if show: 926 uLogger.info("{}".format(infoText)) 927 928 else: 929 uLogger.debug("{}".format(infoText)) 930 931 if self.infoFile is not None: 932 with open(self.infoFile, "w", encoding="UTF-8") as fH: 933 fH.write(infoText) 934 935 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 936 937 return infoText 938 939 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 940 """ 941 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 942 943 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 944 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if self.moreDebug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if self.moreDebug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if self.moreDebug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if self.moreDebug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if self.moreDebug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if self.moreDebug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON 1007 1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1011 1012 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1013 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1014 :return: JSON formatted data with information about instrument. 1015 """ 1016 figiJSON = {} 1017 if self.moreDebug: 1018 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1019 1020 if not self.figi: 1021 uLogger.warning("self.figi variable is not be empty!") 1022 1023 else: 1024 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1025 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1026 raise Exception("Instrument not allowed") 1027 1028 if not self.iList: 1029 self.iList = self.Listing() 1030 1031 for item in self.iList["Shares"].keys(): 1032 if self.figi == self.iList["Shares"][item]["figi"]: 1033 figiJSON = self.iList["Shares"][item] 1034 1035 if self.moreDebug: 1036 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1037 1038 break 1039 1040 if not figiJSON: 1041 for item in self.iList["Currencies"].keys(): 1042 if self.figi == self.iList["Currencies"][item]["figi"]: 1043 figiJSON = self.iList["Currencies"][item] 1044 1045 if self.moreDebug: 1046 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1047 1048 break 1049 1050 if not figiJSON: 1051 for item in self.iList["Bonds"].keys(): 1052 if self.figi == self.iList["Bonds"][item]["figi"]: 1053 figiJSON = self.iList["Bonds"][item] 1054 1055 if self.moreDebug: 1056 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1057 1058 break 1059 1060 if not figiJSON: 1061 for item in self.iList["Etfs"].keys(): 1062 if self.figi == self.iList["Etfs"][item]["figi"]: 1063 figiJSON = self.iList["Etfs"][item] 1064 1065 if self.moreDebug: 1066 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1067 1068 break 1069 1070 if not figiJSON: 1071 for item in self.iList["Futures"].keys(): 1072 if self.figi == self.iList["Futures"][item]["figi"]: 1073 figiJSON = self.iList["Futures"][item] 1074 1075 if self.moreDebug: 1076 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1077 1078 break 1079 1080 if figiJSON: 1081 self.figi = figiJSON["figi"] 1082 self.ticker = figiJSON["ticker"] 1083 1084 if requestPrice: 1085 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1086 1087 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1088 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1089 1090 else: 1091 figiJSON["currentPrice"]["changes"] = 0 1092 1093 if show: 1094 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1095 1096 else: 1097 if show: 1098 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1099 1100 return figiJSON 1101 1102 def GetCurrentPrices(self, show: bool = True) -> dict: 1103 """ 1104 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1105 `{"buy": [{"price": 1243.8, "quantity": 193}, 1106 {"price": 1244.0, "quantity": 168}, 1107 {"price": 1244.8, "quantity": 5}, 1108 {"price": 1245.0, "quantity": 61}, 1109 {"price": 1245.4, "quantity": 60}], 1110 "sell": [{"price": 1243.6, "quantity": 8}, 1111 {"price": 1242.6, "quantity": 10}, 1112 {"price": 1242.4, "quantity": 18}, 1113 {"price": 1242.2, "quantity": 50}, 1114 {"price": 1242.0, "quantity": 113}], 1115 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1116 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1117 - sell: list of dicts with Buyers prices, 1118 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1119 - quantity: volume value by current price in lots, 1120 - limitUp: current trade session limit price, maximum, 1121 - limitDown: current trade session limit price, minimum, 1122 - lastPrice: last deal price of the instrument, 1123 - closePrice: previous trade session close price of the instrument. 1124 1125 See also: `SearchByTicker()` and `SearchByFIGI()`. 1126 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1127 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1128 1129 :param show: if `True` then print DOM to log and console. 1130 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1131 If an error occurred then returns an empty record: 1132 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1133 """ 1134 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1135 1136 if self.depth < 1: 1137 uLogger.error("Depth of Market (DOM) must be >=1!") 1138 raise Exception("Incorrect value") 1139 1140 if not (self.ticker or self.figi): 1141 uLogger.error("self.ticker or self.figi variables must be defined!") 1142 raise Exception("Ticker or FIGI required") 1143 1144 if self.ticker and not self.figi: 1145 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1146 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1147 1148 if not self.ticker and self.figi: 1149 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1150 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1151 1152 if not self.figi: 1153 uLogger.error("FIGI is not defined!") 1154 raise Exception("Ticker or FIGI required") 1155 1156 else: 1157 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1158 1159 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1160 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1161 self.body = str({"figi": self.figi, "depth": self.depth}) 1162 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1163 1164 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1165 # list of dicts with sellers orders: 1166 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1167 1168 # list of dicts with buyers orders: 1169 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1170 1171 # max price of instrument at this time: 1172 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1173 1174 # min price of instrument at this time: 1175 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1176 1177 # last price of deal with instrument: 1178 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1179 1180 # last close price of instrument: 1181 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1182 1183 else: 1184 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1185 uLogger.debug("Server response: {}".format(pricesResponse)) 1186 1187 if show: 1188 if prices["buy"] or prices["sell"]: 1189 info = [ 1190 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1191 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1192 self.ticker, 1193 self.figi, 1194 self.depth, 1195 ), 1196 "-" * 60, "\n", 1197 " Orders of Buyers | Orders of Sellers\n", 1198 "-" * 60, "\n", 1199 " Sell prices (volumes) | Buy prices (volumes)\n", 1200 "-" * 60, "\n", 1201 ] 1202 1203 if not prices["buy"]: 1204 info.append(" | No orders!\n") 1205 sumBuy = 0 1206 1207 else: 1208 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1209 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1210 for item in maxMinSorted: 1211 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1212 1213 if not prices["sell"]: 1214 info.append("No orders! |\n") 1215 sumSell = 0 1216 1217 else: 1218 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1219 for item in prices["sell"]: 1220 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1221 1222 info.extend([ 1223 "-" * 60, "\n", 1224 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1225 "-" * 60, "\n", 1226 ]) 1227 1228 infoText = "".join(info) 1229 1230 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1231 1232 else: 1233 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1234 1235 return prices 1236 1237 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1238 """ 1239 This method get and show information about all available broker instruments for current user account. 1240 If `instrumentsFile` string is not empty then also save information to this file. 1241 1242 :param show: if `True` then print results to console, if `False` — print only to file. 1243 :return: multi-lines string with all available broker instruments 1244 """ 1245 if not self.iList: 1246 self.iList = self.Listing() 1247 1248 info = [ 1249 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1250 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1251 ] 1252 1253 # add instruments count by type: 1254 for iType in self.iList.keys(): 1255 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1256 1257 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1258 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1259 1260 # generating info tables with all instruments by type: 1261 for iType in self.iList.keys(): 1262 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1263 1264 for instrument in self.iList[iType].keys(): 1265 iName = self.iList[iType][instrument]["name"] # instrument's name 1266 if len(iName) > 57: 1267 iName = "{}...".format(iName[:54]) # right trim for a long string 1268 1269 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1270 self.iList[iType][instrument]["ticker"], 1271 iName, 1272 self.iList[iType][instrument]["figi"], 1273 self.iList[iType][instrument]["currency"], 1274 self.iList[iType][instrument]["lot"], 1275 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1276 )) 1277 1278 infoText = "".join(info) 1279 1280 if show: 1281 uLogger.info(infoText) 1282 1283 if self.instrumentsFile: 1284 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1285 fH.write(infoText) 1286 1287 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1288 1289 return infoText 1290 1291 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1292 """ 1293 This method search and show information about instruments by part of its ticker, FIGI or name. 1294 If `searchResultsFile` string is not empty then also save information to this file. 1295 1296 :param pattern: string with part of ticker, FIGI or instrument's name. 1297 :param show: if `True` then print results to console, if `False` — return list of result only. 1298 :return: list of dictionaries with all found instruments. 1299 """ 1300 if not self.iList: 1301 self.iList = self.Listing() 1302 1303 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1304 compiledPattern = re.compile(pattern, re.IGNORECASE) 1305 1306 for iType in self.iList: 1307 for instrument in self.iList[iType].values(): 1308 searchResult = compiledPattern.search(" ".join( 1309 [instrument["ticker"], instrument["figi"], instrument["name"]] 1310 )) 1311 1312 if searchResult: 1313 searchResults[iType][instrument["ticker"]] = instrument 1314 1315 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1316 info = [ 1317 "# Search results\n\n", 1318 "* **Search pattern:** [{}]\n".format(pattern), 1319 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1320 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1321 ] 1322 infoShort = info[:] 1323 1324 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1325 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1326 skippedLine = "| ... | ... | ... | ... |\n" 1327 1328 if resultsLen == 0: 1329 info.append("\nNo results\n") 1330 infoShort.append("\nNo results\n") 1331 uLogger.warning("No results. Try changing your search pattern.") 1332 1333 else: 1334 for iType in searchResults: 1335 iTypeValuesCount = len(searchResults[iType].values()) 1336 if iTypeValuesCount > 0: 1337 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1338 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 1340 for instrument in searchResults[iType].values(): 1341 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1342 instrument["type"], 1343 instrument["ticker"], 1344 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1345 instrument["figi"], 1346 )) 1347 1348 if iTypeValuesCount <= 5: 1349 infoShort.extend(info[-iTypeValuesCount:]) 1350 1351 else: 1352 infoShort.extend(info[-5:]) 1353 infoShort.append(skippedLine) 1354 1355 infoText = "".join(info) 1356 infoTextShort = "".join(infoShort) 1357 1358 if show: 1359 uLogger.info(infoTextShort) 1360 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1361 1362 if self.searchResultsFile: 1363 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1364 fH.write(infoText) 1365 1366 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1367 1368 return searchResults 1369 1370 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1371 """ 1372 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1373 1374 :param instruments: list of strings with tickers or FIGIs. 1375 :return: list with unique instrument FIGIs only. 1376 """ 1377 requestedInstruments = [] 1378 for iName in instruments: 1379 if iName not in self.aliases.keys(): 1380 if iName not in requestedInstruments: 1381 requestedInstruments.append(iName) 1382 1383 else: 1384 if iName not in requestedInstruments: 1385 if self.aliases[iName] not in requestedInstruments: 1386 requestedInstruments.append(self.aliases[iName]) 1387 1388 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1389 1390 onlyUniqueFIGIs = [] 1391 for iName in requestedInstruments: 1392 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1393 continue 1394 1395 self.ticker = iName 1396 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1397 1398 if not iData: 1399 self.ticker = "" 1400 self.figi = iName 1401 1402 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1403 1404 if not iData: 1405 self.figi = "" 1406 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1407 1408 if iData and iData["figi"] not in onlyUniqueFIGIs: 1409 onlyUniqueFIGIs.append(iData["figi"]) 1410 1411 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1412 1413 return onlyUniqueFIGIs 1414 1415 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1416 """ 1417 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1418 1419 See limits: https://tinkoff.github.io/investAPI/limits/ 1420 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList 1444 1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText 1494 1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 1499 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1500 1501 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1502 1503 :return: dictionary with trading status attributes. Response example: 1504 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1505 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1506 """ 1507 if self.figi is None or not self.figi: 1508 uLogger.error("Variable `figi` must be defined for using this method!") 1509 raise Exception("FIGI required") 1510 1511 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1512 1513 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1514 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1515 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1516 1517 if self.moreDebug: 1518 uLogger.debug("Records about current trading status successfully received") 1519 1520 return tradingStatus 1521 1522 def RequestPortfolio(self) -> dict: 1523 """ 1524 Requesting actual user's portfolio for current `accountId`. 1525 1526 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1527 1528 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1529 1530 :return: dictionary with user's portfolio. 1531 """ 1532 if self.accountId is None or not self.accountId: 1533 uLogger.error("Variable `accountId` must be defined for using this method!") 1534 raise Exception("Account ID required") 1535 1536 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1537 1538 self.body = str({"accountId": self.accountId}) 1539 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1540 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1541 1542 if self.moreDebug: 1543 uLogger.debug("Records about user's portfolio successfully received") 1544 1545 return rawPortfolio 1546 1547 def RequestPositions(self) -> dict: 1548 """ 1549 Requesting open positions by currencies and instruments for current `accountId`. 1550 1551 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1552 1553 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1554 1555 :return: dictionary with open positions by instruments. 1556 """ 1557 if self.accountId is None or not self.accountId: 1558 uLogger.error("Variable `accountId` must be defined for using this method!") 1559 raise Exception("Account ID required") 1560 1561 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1562 1563 self.body = str({"accountId": self.accountId}) 1564 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1565 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1566 1567 if self.moreDebug: 1568 uLogger.debug("Records about current open positions successfully received") 1569 1570 return rawPositions 1571 1572 def RequestPendingOrders(self) -> list: 1573 """ 1574 Requesting current actual pending orders for current `accountId`. 1575 1576 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1577 1578 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1579 1580 :return: list of dictionaries with pending orders. 1581 """ 1582 if self.accountId is None or not self.accountId: 1583 uLogger.error("Variable `accountId` must be defined for using this method!") 1584 raise Exception("Account ID required") 1585 1586 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1587 1588 self.body = str({"accountId": self.accountId}) 1589 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1590 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1591 1592 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1593 1594 return rawOrders 1595 1596 def RequestStopOrders(self) -> list: 1597 """ 1598 Requesting current actual stop orders for current `accountId`. 1599 1600 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1601 1602 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1603 1604 :return: list of dictionaries with stop orders. 1605 """ 1606 if self.accountId is None or not self.accountId: 1607 uLogger.error("Variable `accountId` must be defined for using this method!") 1608 raise Exception("Account ID required") 1609 1610 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1611 1612 self.body = str({"accountId": self.accountId}) 1613 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1614 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1615 1616 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1617 1618 return rawStopOrders 1619 1620 def Overview(self, show: bool = False, details: str = "full") -> dict: 1621 """ 1622 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1623 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1624 and `overviewBondsCalendarFile` are defined then also save information to file. 1625 1626 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1627 many requests about the state of the portfolio, and then, based on the received data, a large number 1628 of calculation and statistics are collected. 1629 1630 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1631 :param details: how detailed should the information be? 1632 - `full` — shows full available information about portfolio status (by default), 1633 - `positions` — shows only open positions, 1634 - `orders` — shows only sections of open limits and stop orders. 1635 - `digest` — show a short digest of the portfolio status, 1636 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1637 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1638 :return: dictionary with client's raw portfolio and some statistics. 1639 """ 1640 if self.accountId is None or not self.accountId: 1641 uLogger.error("Variable `accountId` must be defined for using this method!") 1642 raise Exception("Account ID required") 1643 1644 view = { 1645 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1646 "headers": {}, # list of dictionaries, response headers without "positions" section 1647 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1648 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1649 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1650 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1651 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1652 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1653 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1654 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1655 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1656 }, 1657 "stat": { # --- some statistics calculated using "raw" sections: 1658 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1659 "availableRUB": 0., # available rubles (without other currencies) 1660 "blockedRUB": 0., # blocked sum in Russian Rouble 1661 "totalChangesRUB": 0., # changes for all open trades in RUB 1662 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1663 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1664 "sharesCostRUB": 0., # costs of all shares in RUB 1665 "bondsCostRUB": 0., # costs of all bonds in RUB 1666 "etfsCostRUB": 0., # costs of all etfs in RUB 1667 "futuresCostRUB": 0., # costs of all futures in RUB 1668 "Currencies": [], # list of dictionaries of all currencies statistics 1669 "Shares": [], # list of dictionaries of all shares statistics 1670 "Bonds": [], # list of dictionaries of all bonds statistics 1671 "Etfs": [], # list of dictionaries of all etfs statistics 1672 "Futures": [], # list of dictionaries of all futures statistics 1673 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1674 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1675 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1676 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1677 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1678 }, 1679 "analytics": { # --- some analytics of portfolio: 1680 "distrByAssets": {}, # portfolio distribution by assets 1681 "distrByCompanies": {}, # portfolio distribution by companies 1682 "distrBySectors": {}, # portfolio distribution by sectors 1683 "distrByCurrencies": {}, # portfolio distribution by currencies 1684 "distrByCountries": {}, # portfolio distribution by countries 1685 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1686 } 1687 } 1688 1689 details = details.lower() 1690 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1691 if details not in availableDetails: 1692 details = "full" 1693 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1694 1695 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1696 1697 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1698 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1699 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1700 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1701 1702 # save response headers without "positions" section: 1703 for key in portfolioResponse.keys(): 1704 if key != "positions": 1705 view["raw"]["headers"][key] = portfolioResponse[key] 1706 1707 else: 1708 continue 1709 1710 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1711 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1712 for item in portfolioResponse["positions"]: 1713 if item["instrumentType"] == "currency": 1714 self.figi = item["figi"] 1715 curr = self.SearchByFIGI(requestPrice=False) 1716 1717 # current price of currency in RUB: 1718 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1719 "name": curr["name"], 1720 "currentPrice": NanoToFloat( 1721 item["currentPrice"]["units"], 1722 item["currentPrice"]["nano"] 1723 ), 1724 } 1725 1726 view["raw"]["Currencies"].append(item) 1727 1728 elif item["instrumentType"] == "share": 1729 view["raw"]["Shares"].append(item) 1730 1731 elif item["instrumentType"] == "bond": 1732 view["raw"]["Bonds"].append(item) 1733 1734 elif item["instrumentType"] == "etf": 1735 view["raw"]["Etfs"].append(item) 1736 1737 elif item["instrumentType"] == "futures": 1738 view["raw"]["Futures"].append(item) 1739 1740 else: 1741 continue 1742 1743 # how many volume of currencies (by ISO currency name) are blocked: 1744 for item in view["raw"]["positions"]["blocked"]: 1745 blocked = NanoToFloat(item["units"], item["nano"]) 1746 if blocked > 0: 1747 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1748 1749 # how many volume of instruments (by FIGI) are blocked: 1750 for item in view["raw"]["positions"]["securities"]: 1751 blocked = int(item["blocked"]) 1752 if blocked > 0: 1753 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1754 1755 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1756 1757 if "rub" in allBlocked.keys(): 1758 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1759 1760 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1761 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1762 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1763 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1764 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1765 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1766 view["stat"]["portfolioCostRUB"] = sum([ 1767 view["stat"]["allCurrenciesCostRUB"], 1768 view["stat"]["sharesCostRUB"], 1769 view["stat"]["bondsCostRUB"], 1770 view["stat"]["etfsCostRUB"], 1771 view["stat"]["futuresCostRUB"], 1772 ]) 1773 1774 # --- calculating some portfolio statistics: 1775 byComp = {} # distribution by companies 1776 bySect = {} # distribution by sectors 1777 byCurr = {} # distribution by currencies (include RUB) 1778 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1779 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1780 1781 for item in portfolioResponse["positions"]: 1782 self.figi = item["figi"] 1783 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1784 1785 if instrument: 1786 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1787 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1788 1789 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1790 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1791 1792 else: 1793 blocked = 0 1794 1795 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1796 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1797 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1798 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1799 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1800 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1801 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1802 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1803 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1804 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1805 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1806 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1807 1808 statData = { 1809 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1810 "ticker": instrument["ticker"], # ticker by FIGI 1811 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1812 "volume": volume, # available volume of instrument 1813 "lots": lots, # volume in lots of instrument 1814 "direction": direction, # direction of an instrument's position: short or long 1815 "blocked": blocked, # blocked volume of currency or instrument 1816 "currentPrice": curPrice, # current instrument's price in basic asset 1817 "average": average, # current average position price 1818 "cost": cost, # current cost of all volume of instrument in basic asset 1819 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1820 "costRUB": costRUB, # cost of instrument in ruble 1821 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1822 "profit": profit, # expected profit at current moment 1823 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1824 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1825 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1826 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1827 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1828 "step": instrument["step"], # minimum price increment 1829 } 1830 1831 # adding distribution by unique countries: 1832 if statData["country"] not in byCountry.keys(): 1833 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1834 1835 else: 1836 byCountry[statData["country"]]["cost"] += costRUB 1837 byCountry[statData["country"]]["percent"] += percentCostRUB 1838 1839 if item["instrumentType"] != "currency": 1840 # adding distribution by unique companies: 1841 if statData["name"]: 1842 if statData["name"] not in byComp.keys(): 1843 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1844 1845 else: 1846 byComp[statData["name"]]["cost"] += costRUB 1847 byComp[statData["name"]]["percent"] += percentCostRUB 1848 1849 # adding distribution by unique sectors: 1850 if statData["sector"] not in bySect.keys(): 1851 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1852 1853 else: 1854 bySect[statData["sector"]]["cost"] += costRUB 1855 bySect[statData["sector"]]["percent"] += percentCostRUB 1856 1857 # adding distribution by unique currencies: 1858 if currency not in byCurr.keys(): 1859 byCurr[currency] = { 1860 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1861 "cost": costRUB, 1862 "percent": percentCostRUB 1863 } 1864 1865 else: 1866 byCurr[currency]["cost"] += costRUB 1867 byCurr[currency]["percent"] += percentCostRUB 1868 1869 # saving statistics for every instrument: 1870 if item["instrumentType"] == "currency": 1871 view["stat"]["Currencies"].append(statData) 1872 1873 # update dict with free funds for trading (total - blocked) by currencies 1874 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1875 view["stat"]["funds"][currency] = { 1876 "total": volume, 1877 "totalCostRUB": costRUB, # total volume cost in rubles 1878 "free": volume - blocked, 1879 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1880 } 1881 1882 elif item["instrumentType"] == "share": 1883 view["stat"]["Shares"].append(statData) 1884 1885 elif item["instrumentType"] == "bond": 1886 view["stat"]["Bonds"].append(statData) 1887 1888 elif item["instrumentType"] == "etf": 1889 view["stat"]["Etfs"].append(statData) 1890 1891 elif item["instrumentType"] == "Futures": 1892 view["stat"]["Futures"].append(statData) 1893 1894 else: 1895 continue 1896 1897 # total changes in Russian Ruble: 1898 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1899 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1900 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1901 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1902 view["stat"]["funds"]["rub"] = { 1903 "total": view["stat"]["availableRUB"], 1904 "totalCostRUB": view["stat"]["availableRUB"], 1905 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1906 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 } 1908 1909 # --- pending orders sector data: 1910 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1911 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1912 1913 for item in view["raw"]["orders"]: 1914 self.figi = item["figi"] 1915 1916 if item["figi"] not in uniquePendingOrdersFIGIs: 1917 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1918 1919 uniquePendingOrdersFIGIs.append(item["figi"]) 1920 uniquePendingOrders[item["figi"]] = instrument 1921 1922 else: 1923 instrument = uniquePendingOrders[item["figi"]] 1924 1925 if instrument: 1926 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1927 orderType = TKS_ORDER_TYPES[item["orderType"]] 1928 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1929 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1930 1931 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1932 if item["direction"] == "ORDER_DIRECTION_BUY": 1933 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1934 1935 else: 1936 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1937 1938 # requested price for order execution: 1939 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1940 1941 # necessary changes in percent to reach target from current price: 1942 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1943 1944 view["stat"]["orders"].append({ 1945 "orderID": item["orderId"], # orderId number parameter of current order 1946 "figi": item["figi"], # FIGI identification 1947 "ticker": instrument["ticker"], # ticker name by FIGI 1948 "lotsRequested": item["lotsRequested"], # requested lots value 1949 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1950 "currentPrice": lastPrice, # current instrument's price for defined action 1951 "targetPrice": target, # requested price for order execution in base currency 1952 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1953 "percentChanges": changes, # changes in percent to target from current price 1954 "currency": item["currency"], # instrument's currency name 1955 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1956 "type": orderType, # type of order from TKS_ORDER_TYPES 1957 "status": orderState, # order status from TKS_ORDER_STATES 1958 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1959 }) 1960 1961 # --- stop orders sector data: 1962 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1963 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1964 1965 for item in view["raw"]["stopOrders"]: 1966 self.figi = item["figi"] 1967 1968 if item["figi"] not in uniqueStopOrdersFIGIs: 1969 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1970 1971 uniqueStopOrdersFIGIs.append(item["figi"]) 1972 uniqueStopOrders[item["figi"]] = instrument 1973 1974 else: 1975 instrument = uniqueStopOrders[item["figi"]] 1976 1977 if instrument: 1978 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1979 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1980 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1981 1982 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1983 if "expirationTime" in item.keys(): 1984 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1985 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1986 1987 else: 1988 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1989 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1990 1991 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1992 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1993 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1994 1995 else: 1996 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1997 1998 # requested price when stop-order executed: 1999 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2000 2001 # price for limit-order, set up when stop-order executed: 2002 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2003 2004 # necessary changes in percent to reach target from current price: 2005 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2006 2007 view["stat"]["stopOrders"].append({ 2008 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2009 "figi": item["figi"], # FIGI identification 2010 "ticker": instrument["ticker"], # ticker name by FIGI 2011 "lotsRequested": item["lotsRequested"], # requested lots value 2012 "currentPrice": lastPrice, # current instrument's price for defined action 2013 "targetPrice": target, # requested price for stop-order execution in base currency 2014 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2015 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2016 "percentChanges": changes, # changes in percent to target from current price 2017 "currency": item["currency"], # instrument's currency name 2018 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2019 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2020 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2021 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2022 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2023 }) 2024 2025 # --- calculating data for analytics section: 2026 # portfolio distribution by assets: 2027 view["analytics"]["distrByAssets"] = { 2028 "Ruble": { 2029 "uniques": 1, 2030 "cost": view["stat"]["availableRUB"], 2031 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2032 }, 2033 "Currencies": { 2034 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2035 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2036 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2037 }, 2038 "Shares": { 2039 "uniques": len(view["stat"]["Shares"]), 2040 "cost": view["stat"]["sharesCostRUB"], 2041 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 }, 2043 "Bonds": { 2044 "uniques": len(view["stat"]["Bonds"]), 2045 "cost": view["stat"]["bondsCostRUB"], 2046 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2047 }, 2048 "Etfs": { 2049 "uniques": len(view["stat"]["Etfs"]), 2050 "cost": view["stat"]["etfsCostRUB"], 2051 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2052 }, 2053 "Futures": { 2054 "uniques": len(view["stat"]["Futures"]), 2055 "cost": view["stat"]["futuresCostRUB"], 2056 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2057 }, 2058 } 2059 2060 # portfolio distribution by companies: 2061 view["analytics"]["distrByCompanies"]["All money cash"] = { 2062 "ticker": "", 2063 "cost": view["stat"]["allCurrenciesCostRUB"], 2064 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2065 } 2066 view["analytics"]["distrByCompanies"].update(byComp) 2067 2068 # portfolio distribution by sectors: 2069 view["analytics"]["distrBySectors"]["All money cash"] = { 2070 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2071 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2072 } 2073 view["analytics"]["distrBySectors"].update(bySect) 2074 2075 # portfolio distribution by currencies: 2076 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2077 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2078 2079 if self.moreDebug: 2080 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2081 2082 view["analytics"]["distrByCurrencies"].update(byCurr) 2083 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2084 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2085 2086 # portfolio distribution by countries: 2087 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2088 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2089 2090 if self.moreDebug: 2091 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2092 2093 view["analytics"]["distrByCountries"].update(byCountry) 2094 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2096 2097 # --- Prepare text statistics overview in human-readable: 2098 if show: 2099 # Whatever the value `details`, header not changes: 2100 info = [ 2101 "# Client's portfolio\n\n", 2102 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2103 "* **Account ID:** [{}]\n".format(self.accountId), 2104 ] 2105 2106 if details in ["full", "positions", "digest"]: 2107 info.extend([ 2108 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2109 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2110 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2111 view["stat"]["totalChangesRUB"], 2112 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2113 view["stat"]["totalChangesPercentRUB"], 2114 ), 2115 ]) 2116 2117 if details in ["full", "positions"]: 2118 info.extend([ 2119 "## Open positions\n\n", 2120 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2121 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2122 "| Ruble | {:>31} | | | | | |\n".format( 2123 "{:.2f} ({:.2f}) rub".format( 2124 view["stat"]["availableRUB"], 2125 view["stat"]["blockedRUB"], 2126 ) 2127 ) 2128 ]) 2129 2130 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2131 return [ 2132 "| | | | | | | |\n", 2133 "| {:<27} | | | | | {:>19} | |\n".format( 2134 noTradeStr if noTradeStr else typeStr, 2135 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2136 ), 2137 ] 2138 2139 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2140 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2141 "{} [{}]".format(data["ticker"], data["figi"]), 2142 "{:.2f} ({:.2f}) {}".format( 2143 data["volume"], 2144 data["blocked"], 2145 data["currency"], 2146 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2147 data["volume"], 2148 data["blocked"], 2149 ), 2150 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2151 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2152 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2153 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2154 "{}{:.2f} {} ({}{:.2f}%)".format( 2155 "+" if data["profit"] > 0 else "", 2156 data["profit"], data["baseCurrencyName"], 2157 "+" if data["percentProfit"] > 0 else "", 2158 data["percentProfit"], 2159 ), 2160 ) 2161 2162 # --- Show currencies section: 2163 if view["stat"]["Currencies"]: 2164 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2165 for item in view["stat"]["Currencies"]: 2166 info.append(_InfoStr(item, showCurrencyName=True)) 2167 2168 else: 2169 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2170 2171 # --- Show shares section: 2172 if view["stat"]["Shares"]: 2173 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2174 2175 for item in view["stat"]["Shares"]: 2176 info.append(_InfoStr(item)) 2177 2178 else: 2179 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2180 2181 # --- Show bonds section: 2182 if view["stat"]["Bonds"]: 2183 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2184 2185 for item in view["stat"]["Bonds"]: 2186 info.append(_InfoStr(item)) 2187 2188 else: 2189 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2190 2191 # --- Show etfs section: 2192 if view["stat"]["Etfs"]: 2193 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2194 2195 for item in view["stat"]["Etfs"]: 2196 info.append(_InfoStr(item)) 2197 2198 else: 2199 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2200 2201 # --- Show futures section: 2202 if view["stat"]["Futures"]: 2203 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2204 2205 for item in view["stat"]["Futures"]: 2206 info.append(_InfoStr(item)) 2207 2208 else: 2209 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2210 2211 if details in ["full", "orders"]: 2212 # --- Show pending orders section: 2213 if view["stat"]["orders"]: 2214 info.extend([ 2215 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2216 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2217 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2218 ]) 2219 2220 for item in view["stat"]["orders"]: 2221 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2222 "{} [{}]".format(item["ticker"], item["figi"]), 2223 item["orderID"], 2224 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2225 "{} {} ({}{:.2f}%)".format( 2226 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2227 item["baseCurrencyName"], 2228 "+" if item["percentChanges"] > 0 else "", 2229 float(item["percentChanges"]), 2230 ), 2231 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2232 item["action"], 2233 item["type"], 2234 item["date"], 2235 )) 2236 2237 else: 2238 info.append("\n## Total pending limit-orders: 0\n") 2239 2240 # --- Show stop orders section: 2241 if view["stat"]["stopOrders"]: 2242 info.extend([ 2243 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2244 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2245 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2246 ]) 2247 2248 for item in view["stat"]["stopOrders"]: 2249 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2250 "{} [{}]".format(item["ticker"], item["figi"]), 2251 item["orderID"], 2252 item["lotsRequested"], 2253 "{} {} ({}{:.2f}%)".format( 2254 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2255 item["baseCurrencyName"], 2256 "+" if item["percentChanges"] > 0 else "", 2257 float(item["percentChanges"]), 2258 ), 2259 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2260 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2261 item["action"], 2262 item["type"], 2263 item["expType"], 2264 item["createDate"], 2265 item["expDate"], 2266 )) 2267 2268 else: 2269 info.append("\n## Total stop-orders: 0\n") 2270 2271 if details in ["full", "analytics"]: 2272 # -- Show analytics section: 2273 if view["stat"]["portfolioCostRUB"] > 0: 2274 info.extend([ 2275 "\n# Analytics\n" 2276 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2277 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2278 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2279 view["stat"]["totalChangesRUB"], 2280 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2281 view["stat"]["totalChangesPercentRUB"], 2282 ), 2283 "\n## Portfolio distribution by assets\n" 2284 "\n| Type | Uniques | Percent | Current cost |\n", 2285 "|------------------------------------|---------|---------|--------------------|\n", 2286 ]) 2287 2288 for key in view["analytics"]["distrByAssets"].keys(): 2289 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2290 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2291 key, 2292 view["analytics"]["distrByAssets"][key]["uniques"], 2293 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2294 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2295 )) 2296 2297 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2298 2299 info.extend([ 2300 "\n## Portfolio distribution by companies\n" 2301 "\n| Company | Percent | Current cost |\n", 2302 aSepLine, 2303 ]) 2304 2305 for company in view["analytics"]["distrByCompanies"].keys(): 2306 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2307 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2308 "{}{}".format( 2309 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2310 company, 2311 ), 2312 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2313 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2314 )) 2315 2316 info.extend([ 2317 "\n## Portfolio distribution by sectors\n" 2318 "\n| Sector | Percent | Current cost |\n", 2319 aSepLine, 2320 ]) 2321 2322 for sector in view["analytics"]["distrBySectors"].keys(): 2323 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2324 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2325 sector, 2326 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2327 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2328 )) 2329 2330 info.extend([ 2331 "\n## Portfolio distribution by currencies\n" 2332 "\n| Instruments currencies | Percent | Current cost |\n", 2333 aSepLine, 2334 ]) 2335 2336 for curr in view["analytics"]["distrByCurrencies"].keys(): 2337 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2338 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2339 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2340 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2341 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2342 )) 2343 2344 info.extend([ 2345 "\n## Portfolio distribution by countries\n" 2346 "\n| Assets by country | Percent | Current cost |\n", 2347 aSepLine, 2348 ]) 2349 2350 for country in view["analytics"]["distrByCountries"].keys(): 2351 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2352 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2353 country, 2354 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2355 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2356 )) 2357 2358 if details in ["full", "calendar"]: 2359 # -- Show bonds payment calendar section: 2360 if view["stat"]["Bonds"]: 2361 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2362 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2363 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2364 2365 else: 2366 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2367 2368 infoText = "".join(info) 2369 2370 uLogger.info(infoText) 2371 2372 if details == "full" and self.overviewFile: 2373 filename = self.overviewFile 2374 2375 elif details == "digest" and self.overviewDigestFile: 2376 filename = self.overviewDigestFile 2377 2378 elif details == "positions" and self.overviewPositionsFile: 2379 filename = self.overviewPositionsFile 2380 2381 elif details == "orders" and self.overviewOrdersFile: 2382 filename = self.overviewOrdersFile 2383 2384 elif details == "analytics" and self.overviewAnalyticsFile: 2385 filename = self.overviewAnalyticsFile 2386 2387 elif details == "calendar" and self.overviewBondsCalendarFile: 2388 filename = self.overviewBondsCalendarFile 2389 2390 else: 2391 filename = "" 2392 2393 if filename: 2394 with open(filename, "w", encoding="UTF-8") as fH: 2395 fH.write(infoText) 2396 2397 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2398 2399 return view 2400 2401 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2402 """ 2403 Returns history operations between two given dates for current `accountId`. 2404 If `reportFile` string is not empty then also save human-readable report. 2405 Shows some statistical data of closed positions. 2406 2407 :param start: see docstring in `GetDatesAsString()` method 2408 :param end: see docstring in `GetDatesAsString()` method 2409 :param show: if `True` then also prints all records to the console. 2410 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2411 :return: original list of dictionaries with history of deals records from API ("operations" key): 2412 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2413 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2414 """ 2415 if self.accountId is None or not self.accountId: 2416 uLogger.error("Variable `accountId` must be defined for using this method!") 2417 raise Exception("Account ID required") 2418 2419 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2420 2421 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2422 2423 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2424 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2425 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2426 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2427 customStat = {} # custom statistics in additional to responseJSON 2428 2429 # --- output report in human-readable format: 2430 if show or self.reportFile: 2431 splitLine1 = "| | | | | |\n" # Summary section 2432 splitLine2 = "| | | | | | | | |\n" # Operations section 2433 nextDay = "" 2434 2435 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2436 2437 if len(ops) > 0: 2438 customStat = { 2439 "opsCount": 0, # total operations count 2440 "buyCount": 0, # buy operations 2441 "sellCount": 0, # sell operations 2442 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2443 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2444 "payIn": {"rub": 0.}, # Deposit brokerage account 2445 "payOut": {"rub": 0.}, # Withdrawals 2446 "divs": {"rub": 0.}, # Dividends income 2447 "coupons": {"rub": 0.}, # Coupon's income 2448 "brokerCom": {"rub": 0.}, # Service commissions 2449 "serviceCom": {"rub": 0.}, # Service commissions 2450 "marginCom": {"rub": 0.}, # Margin commissions 2451 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2452 } 2453 2454 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2455 for item in ops: 2456 if item["state"] == "OPERATION_STATE_EXECUTED": 2457 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2458 2459 # count buy operations: 2460 if "_BUY" in item["operationType"]: 2461 customStat["buyCount"] += 1 2462 2463 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2464 customStat["buyTotal"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["buyTotal"][item["payment"]["currency"]] = payment 2468 2469 # count sell operations: 2470 elif "_SELL" in item["operationType"]: 2471 customStat["sellCount"] += 1 2472 2473 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2474 customStat["sellTotal"][item["payment"]["currency"]] += payment 2475 2476 else: 2477 customStat["sellTotal"][item["payment"]["currency"]] = payment 2478 2479 # count incoming operations: 2480 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2481 if item["payment"]["currency"] in customStat["payIn"].keys(): 2482 customStat["payIn"][item["payment"]["currency"]] += payment 2483 2484 else: 2485 customStat["payIn"][item["payment"]["currency"]] = payment 2486 2487 # count withdrawals operations: 2488 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2489 if item["payment"]["currency"] in customStat["payOut"].keys(): 2490 customStat["payOut"][item["payment"]["currency"]] += payment 2491 2492 else: 2493 customStat["payOut"][item["payment"]["currency"]] = payment 2494 2495 # count dividends income: 2496 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2497 if item["payment"]["currency"] in customStat["divs"].keys(): 2498 customStat["divs"][item["payment"]["currency"]] += payment 2499 2500 else: 2501 customStat["divs"][item["payment"]["currency"]] = payment 2502 2503 # count coupon's income: 2504 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2505 if item["payment"]["currency"] in customStat["coupons"].keys(): 2506 customStat["coupons"][item["payment"]["currency"]] += payment 2507 2508 else: 2509 customStat["coupons"][item["payment"]["currency"]] = payment 2510 2511 # count broker commissions: 2512 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2513 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2514 customStat["brokerCom"][item["payment"]["currency"]] += payment 2515 2516 else: 2517 customStat["brokerCom"][item["payment"]["currency"]] = payment 2518 2519 # count service commissions: 2520 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2521 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2522 customStat["serviceCom"][item["payment"]["currency"]] += payment 2523 2524 else: 2525 customStat["serviceCom"][item["payment"]["currency"]] = payment 2526 2527 # count margin commissions: 2528 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2529 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2530 customStat["marginCom"][item["payment"]["currency"]] += payment 2531 2532 else: 2533 customStat["marginCom"][item["payment"]["currency"]] = payment 2534 2535 # count withholding taxes: 2536 elif "_TAX" in item["operationType"]: 2537 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2538 customStat["allTaxes"][item["payment"]["currency"]] += payment 2539 2540 else: 2541 customStat["allTaxes"][item["payment"]["currency"]] = payment 2542 2543 else: 2544 continue 2545 2546 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2547 2548 # --- view "Actions" lines: 2549 info.extend([ 2550 "| Report sections | | | | |\n", 2551 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2552 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2553 "| | Buy: {:<22} | {:<28} | | |\n".format( 2554 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2555 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2556 ), 2557 "| | Sell: {:<21} | {:<28} | | |\n".format( 2558 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2559 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2560 ), 2561 ]) 2562 2563 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2564 for key in opsKeys: 2565 if key == "rub": 2566 continue 2567 2568 info.extend([ 2569 "| | | {:<28} | | |\n".format( 2570 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2571 ), 2572 "| | | {:<28} | | |\n".format( 2573 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2574 ), 2575 ]) 2576 2577 info.append(splitLine1) 2578 2579 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2580 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2581 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2582 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2583 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2584 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2585 ) 2586 2587 # --- view "Payments" lines: 2588 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2589 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2590 2591 for key in paymentsKeys: 2592 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2593 2594 info.append(splitLine1) 2595 2596 # --- view "Commissions and taxes" lines: 2597 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2598 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2599 2600 for key in comKeys: 2601 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2602 2603 info.append(splitLine1) 2604 2605 info.extend([ 2606 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2607 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2608 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2609 ]) 2610 2611 else: 2612 info.append("Broker returned no operations during this period\n") 2613 2614 # --- view "Operations" section: 2615 for item in ops: 2616 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2617 continue 2618 2619 else: 2620 self.figi = item["figi"] if item["figi"] else "" 2621 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2622 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2623 2624 # group of deals during one day: 2625 if nextDay and item["date"].split("T")[0] != nextDay: 2626 info.append(splitLine2) 2627 nextDay = "" 2628 2629 else: 2630 nextDay = item["date"].split("T")[0] # saving current day for splitting 2631 2632 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2633 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2634 self.figi if self.figi else "—", 2635 instrument["ticker"] if instrument else "—", 2636 instrument["type"] if instrument else "—", 2637 item["quantity"] if int(item["quantity"]) > 0 else "—", 2638 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2639 TKS_OPERATION_STATES[item["state"]], 2640 TKS_OPERATION_TYPES[item["operationType"]], 2641 )) 2642 2643 infoText = "".join(info) 2644 2645 if show: 2646 if self.moreDebug: 2647 uLogger.debug("Records about history of a client's operations successfully received") 2648 2649 uLogger.info(infoText) 2650 2651 if self.reportFile: 2652 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2653 fH.write(infoText) 2654 2655 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2656 2657 return ops, customStat 2658 2659 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2660 """ 2661 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2662 2663 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2664 Warning! Broker server used ISO UTC time by default. 2665 2666 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2667 Also, `historyFile` used to update history with `onlyMissing` parameter. 2668 2669 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2670 2671 :param start: see docstring in `GetDatesAsString()` method. 2672 :param end: see docstring in `GetDatesAsString()` method. 2673 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2674 `"hour"`, `"day"`. Default: `"hour"`. 2675 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2676 False by default. Warning! History appends only from last candle to current time 2677 with always update last candle! 2678 :param csvSep: separator if csv-file is used, `,` by default. 2679 :param show: if `True` then also prints Pandas DataFrame to the console. 2680 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2681 `["date", "time", "open", "high", "low", "close", "volume"]`. 2682 """ 2683 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2684 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2685 history = None # empty pandas object for history 2686 2687 if interval not in TKS_CANDLE_INTERVALS.keys(): 2688 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2689 raise Exception("Incorrect value") 2690 2691 if not (self.ticker or self.figi): 2692 uLogger.error("Ticker or FIGI must be defined!") 2693 raise Exception("Ticker or FIGI required") 2694 2695 if self.ticker and not self.figi: 2696 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2697 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2698 2699 if self.figi and not self.ticker: 2700 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2701 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2702 2703 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2704 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2705 if interval.lower() != "day": 2706 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2707 2708 delta = dtEnd - dtStart # current UTC time minus last time in file 2709 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2710 2711 # calculate history length in candles: 2712 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2713 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2714 length += 1 # to avoid fraction time 2715 2716 # calculate data blocks count: 2717 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2718 2719 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2720 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2721 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2722 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2723 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2724 2725 tempOld = None # pandas object for old history, if --only-missing key present 2726 lastTime = None # datetime object of last old candle in file 2727 2728 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2729 uLogger.debug("--only-missing key present, add only last missing candles...") 2730 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2731 2732 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2733 2734 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2735 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2736 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2737 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2738 2739 # get last datetime object from last string in file or minus 1 delta if file is empty: 2740 if len(tempOld) > 0: 2741 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2742 2743 else: 2744 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2745 2746 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2747 2748 responseJSONs = [] # raw history blocks of data 2749 2750 blockEnd = dtEnd 2751 for item in range(blocks): 2752 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2753 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2754 2755 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2756 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2757 )) 2758 2759 if blockStart == blockEnd: 2760 uLogger.debug("Skipped this zero-length block...") 2761 2762 else: 2763 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2764 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2765 self.body = str({ 2766 "figi": self.figi, 2767 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2768 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2769 "interval": TKS_CANDLE_INTERVALS[interval][0] 2770 }) 2771 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2772 2773 if "code" in responseJSON.keys(): 2774 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2775 2776 else: 2777 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2778 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2779 2780 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2781 2782 blockEnd = blockStart 2783 2784 printCount = len(responseJSONs) # candles to show in console 2785 if responseJSONs: 2786 tempHistory = pd.DataFrame( 2787 data={ 2788 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2789 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2790 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2791 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2792 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2793 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2794 "volume": [int(item["volume"]) for item in responseJSONs], 2795 }, 2796 index=range(len(responseJSONs)), 2797 columns=["date", "time", "open", "high", "low", "close", "volume"], 2798 ) 2799 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2800 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2801 2802 # append only newest candles to old history if --only-missing key present: 2803 if onlyMissing and tempOld is not None and lastTime is not None: 2804 index = 0 # find start index in tempHistory data: 2805 2806 for i, item in tempHistory.iterrows(): 2807 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2808 2809 if curTime == lastTime: 2810 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2811 index = i 2812 printCount = index + 1 2813 break 2814 2815 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2816 2817 else: 2818 history = tempHistory # if no `--only-missing` key then load full data from server 2819 2820 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2821 2822 if history is not None and not history.empty: 2823 if show: 2824 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2825 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2826 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2827 )) 2828 2829 else: 2830 uLogger.warning("Received an empty candles history!") 2831 2832 if self.historyFile is not None: 2833 if history is not None and not history.empty: 2834 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2835 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2836 2837 else: 2838 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2839 2840 else: 2841 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2842 2843 return history 2844 2845 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2846 """ 2847 Load candles history from csv-file and return Pandas DataFrame object. 2848 2849 See also: `History()` and `ShowHistoryChart()` methods. 2850 2851 :param filePath: path to csv-file to open. 2852 """ 2853 loadedHistory = None # init candles data object 2854 2855 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2856 2857 if os.path.exists(filePath): 2858 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2859 2860 tfStr = self.priceModel.FormattedDelta( 2861 self.priceModel.timeframe, 2862 "{days} days {hours}h {minutes}m {seconds}s", 2863 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2864 self.priceModel.timeframe, 2865 "{hours}h {minutes}m {seconds}s", 2866 ) 2867 2868 if loadedHistory is not None and not loadedHistory.empty: 2869 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2870 len(loadedHistory), 2871 tfStr, 2872 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2873 ) 2874 2875 else: 2876 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2877 2878 else: 2879 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2880 2881 return loadedHistory 2882 2883 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2884 """ 2885 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2886 2887 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2888 Default: `index.html` (both for interact and non-interact candlesticks chart). 2889 2890 See also: `History()` and `LoadHistory()` methods. 2891 2892 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2893 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2894 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2895 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2896 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2897 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2898 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2899 """ 2900 if isinstance(candles, str): 2901 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2902 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2903 2904 elif isinstance(candles, pd.DataFrame): 2905 self.priceModel.prices = candles # set candles chain from variable 2906 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2907 2908 if "datetime" not in candles.columns: 2909 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2910 2911 else: 2912 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2913 raise Exception("Incorrect value") 2914 2915 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2916 2917 if interact: 2918 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2919 2920 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2921 2922 else: 2923 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2924 2925 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2926 2927 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2928 2929 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2930 """ 2931 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2932 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2933 2934 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2935 2936 :param operation: string "Buy" or "Sell". 2937 :param lots: volume, integer count of lots >= 1. 2938 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2939 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2940 :param expDate: string "Undefined" by default or local date in future, 2941 it is a string with format `%Y-%m-%d %H:%M:%S`. 2942 :return: JSON with response from broker server. 2943 """ 2944 if self.accountId is None or not self.accountId: 2945 uLogger.error("Variable `accountId` must be defined for using this method!") 2946 raise Exception("Account ID required") 2947 2948 if operation is None or not operation or operation not in ("Buy", "Sell"): 2949 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2950 raise Exception("Incorrect value") 2951 2952 if lots is None or lots < 1: 2953 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2954 lots = 1 2955 2956 if tp is None or tp < 0: 2957 tp = 0 2958 2959 if sl is None or sl < 0: 2960 sl = 0 2961 2962 if expDate is None or not expDate: 2963 expDate = "Undefined" 2964 2965 if not (self.ticker or self.figi): 2966 uLogger.error("Ticker or FIGI must be defined!") 2967 raise Exception("Ticker or FIGI required") 2968 2969 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2970 self.ticker = instrument["ticker"] 2971 self.figi = instrument["figi"] 2972 2973 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2974 2975 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2976 self.body = str({ 2977 "figi": self.figi, 2978 "quantity": str(lots), 2979 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2980 "accountId": str(self.accountId), 2981 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2982 }) 2983 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2984 2985 if "orderId" in response.keys(): 2986 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2987 operation, response["orderId"], 2988 self.ticker, self.figi, lots, 2989 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2990 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2991 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2992 )) 2993 2994 if tp > 0: 2995 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2996 2997 if sl > 0: 2998 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2999 3000 else: 3001 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3002 3003 return response 3004 3005 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3006 """ 3007 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3008 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3009 3010 See also: `Order()` and `Trade()` docstrings. 3011 3012 :param lots: volume, integer count of lots >= 1. 3013 :param tp: float > 0, take profit price of stop-order. 3014 :param sl: float > 0, stop loss price of stop-order. 3015 :param expDate: it's a local date in future. 3016 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3017 :return: JSON with response from broker server. 3018 """ 3019 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3020 3021 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3022 """ 3023 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3024 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3025 3026 See also: `Order()` and `Trade()` docstrings. 3027 3028 :param lots: volume, integer count of lots >= 1. 3029 :param tp: float > 0, take profit price of stop-order. 3030 :param sl: float > 0, stop loss price of stop-order. 3031 :param expDate: it's a local date in the future. 3032 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3033 :return: JSON with response from broker server. 3034 """ 3035 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3036 3037 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3038 """ 3039 Close position of given instruments. 3040 3041 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3042 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3043 This avoids unnecessary downloading data from the server. 3044 """ 3045 if instruments is None or not instruments: 3046 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3047 raise Exception("Ticker or FIGI required") 3048 3049 if isinstance(instruments, str): 3050 instruments = [instruments] 3051 3052 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3053 if uniqueInstruments: 3054 if portfolio is None or not portfolio: 3055 portfolio = self.Overview(show=False) 3056 3057 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3058 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3059 3060 for self.figi in uniqueInstruments: 3061 if self.figi not in allOpened: 3062 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3063 continue 3064 3065 # search open trade info about instrument by ticker: 3066 instrument = {} 3067 for iType in TKS_INSTRUMENTS: 3068 if instrument: 3069 break 3070 3071 for item in portfolio["stat"][iType]: 3072 if item["figi"] == self.figi: 3073 instrument = item 3074 break 3075 3076 if instrument: 3077 self.ticker = instrument["ticker"] 3078 self.figi = instrument["figi"] 3079 3080 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3081 self.ticker, 3082 self.figi, 3083 int(instrument["volume"]), 3084 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3085 )) 3086 3087 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3088 3089 if tradeLots > 0: 3090 if instrument["blocked"] > 0: 3091 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3092 instrument["blocked"], 3093 self.ticker, 3094 tradeLots, 3095 )) 3096 3097 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3098 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3099 3100 else: 3101 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3102 3103 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3104 """ 3105 Close all positions of given instruments with defined type. 3106 3107 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3108 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3109 This avoids unnecessary downloading data from the server. 3110 """ 3111 if iType not in TKS_INSTRUMENTS: 3112 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3113 3114 else: 3115 if portfolio is None or not portfolio: 3116 portfolio = self.Overview(show=False) 3117 3118 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3119 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3120 3121 if tickers and portfolio: 3122 self.CloseTrades(tickers, portfolio) 3123 3124 else: 3125 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3126 3127 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3128 """ 3129 Universal method to create market or limit orders with all available parameters for current `accountId`. 3130 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3131 3132 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3133 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3134 3135 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3136 then broker immediately open market order as you can do simple --buy or --sell operations! 3137 3138 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3139 When current price will go up or down to target price value then broker opens a limit order. 3140 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3141 3142 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3143 3144 :param operation: string "Buy" or "Sell". 3145 :param orderType: string "Limit" or "Stop". 3146 :param lots: volume, integer count of lots >= 1. 3147 :param targetPrice: target price > 0. This is open trade price for limit order. 3148 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3149 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3150 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3151 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3152 Stop loss order always executed by market price. 3153 :param expDate: string "Undefined" by default or local date in future. 3154 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3155 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3156 A limit order has no expiration date, it lasts until the end of the trading day. 3157 :return: JSON with response from broker server. 3158 """ 3159 if self.accountId is None or not self.accountId: 3160 uLogger.error("Variable `accountId` must be defined for using this method!") 3161 raise Exception("Account ID required") 3162 3163 if operation is None or not operation or operation not in ("Buy", "Sell"): 3164 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3165 raise Exception("Incorrect value") 3166 3167 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3168 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3169 raise Exception("Incorrect value") 3170 3171 if lots is None or lots < 1: 3172 uLogger.error("You must define trade volume > 0: integer count of lots!") 3173 raise Exception("Incorrect value") 3174 3175 if targetPrice is None or targetPrice <= 0: 3176 uLogger.error("Target price for limit-order must be greater than 0!") 3177 raise Exception("Incorrect value") 3178 3179 if limitPrice is None or limitPrice <= 0: 3180 limitPrice = targetPrice 3181 3182 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3183 stopType = "Limit" 3184 3185 if expDate is None or not expDate: 3186 expDate = "Undefined" 3187 3188 if not (self.ticker or self.figi): 3189 uLogger.error("Tocker or FIGI must be defined!") 3190 raise Exception("Ticker or FIGI required") 3191 3192 response = {} 3193 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3194 self.ticker = instrument["ticker"] 3195 self.figi = instrument["figi"] 3196 3197 if orderType == "Limit": 3198 uLogger.debug( 3199 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3200 self.ticker, self.figi, 3201 operation, lots, targetPrice, instrument["currency"], 3202 )) 3203 3204 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3205 self.body = str({ 3206 "figi": self.figi, 3207 "quantity": str(lots), 3208 "price": FloatToNano(targetPrice), 3209 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3210 "accountId": str(self.accountId), 3211 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3212 }) 3213 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3214 3215 if "orderId" in response.keys(): 3216 uLogger.info( 3217 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3218 response["orderId"], 3219 self.ticker, self.figi, 3220 operation, lots, targetPrice, instrument["currency"], 3221 )) 3222 3223 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3224 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3225 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3226 targetPrice, instrument["currency"], 3227 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3228 )) 3229 3230 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3231 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3232 targetPrice, instrument["currency"], 3233 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3234 )) 3235 3236 else: 3237 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3238 3239 if orderType == "Stop": 3240 uLogger.debug( 3241 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3242 self.ticker, self.figi, 3243 operation, lots, 3244 targetPrice, instrument["currency"], 3245 limitPrice, instrument["currency"], 3246 stopType, expDate, 3247 )) 3248 3249 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3250 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3251 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3252 3253 body = { 3254 "figi": self.figi, 3255 "quantity": str(lots), 3256 "price": FloatToNano(limitPrice), 3257 "stopPrice": FloatToNano(targetPrice), 3258 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3259 "accountId": str(self.accountId), 3260 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3261 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3262 } 3263 3264 if expDateUTC: 3265 body["expireDate"] = expDateUTC 3266 3267 self.body = str(body) 3268 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3269 3270 if "stopOrderId" in response.keys(): 3271 uLogger.info( 3272 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3273 response["stopOrderId"], 3274 self.ticker, self.figi, 3275 operation, lots, 3276 targetPrice, instrument["currency"], 3277 limitPrice, instrument["currency"], 3278 TKS_STOP_ORDER_TYPES[stopOrderType], 3279 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3280 )) 3281 3282 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3283 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3284 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3285 targetPrice, instrument["currency"], 3286 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3287 )) 3288 3289 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3290 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3291 targetPrice, instrument["currency"], 3292 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3293 )) 3294 3295 else: 3296 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3297 3298 return response 3299 3300 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3301 """ 3302 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3303 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3304 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3305 See also: `Order()` docstring. 3306 3307 :param lots: volume, integer count of lots >= 1. 3308 :param targetPrice: target price > 0. This is open trade price for limit order. 3309 :return: JSON with response from broker server. 3310 """ 3311 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3312 3313 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3314 """ 3315 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3316 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3317 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3318 target price value then broker opens a limit order. See also: `Order()` docstring. 3319 3320 :param lots: volume, integer count of lots >= 1. 3321 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3322 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3323 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3324 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3325 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3326 :param expDate: string "Undefined" by default or local date in future. 3327 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3328 This date is converting to UTC format for server. 3329 :return: JSON with response from broker server. 3330 """ 3331 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3332 3333 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3334 """ 3335 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3336 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3337 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3338 See also: `Order()` docstring. 3339 3340 :param lots: volume, integer count of lots >= 1. 3341 :param targetPrice: target price > 0. This is open trade price for limit order. 3342 :return: JSON with response from broker server. 3343 """ 3344 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3345 3346 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3347 """ 3348 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3349 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3350 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3351 target price value then broker opens a limit order. See also: `Order()` docstring. 3352 3353 :param lots: volume, integer count of lots >= 1. 3354 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3355 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3356 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3357 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3358 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3359 :param expDate: string "Undefined" by default or local date in future. 3360 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3361 This date is converting to UTC format for server. 3362 :return: JSON with response from broker server. 3363 """ 3364 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3365 3366 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3367 """ 3368 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3369 3370 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3371 :param allOrdersIDs: pre-received lists of all active pending orders. 3372 This avoids unnecessary downloading data from the server. 3373 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3374 """ 3375 if self.accountId is None or not self.accountId: 3376 uLogger.error("Variable `accountId` must be defined for using this method!") 3377 raise Exception("Account ID required") 3378 3379 if orderIDs: 3380 if allOrdersIDs is None or not allOrdersIDs: 3381 rawOrders = self.RequestPendingOrders() 3382 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3383 3384 if allStopOrdersIDs is None or not allStopOrdersIDs: 3385 rawStopOrders = self.RequestStopOrders() 3386 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3387 3388 for orderID in orderIDs: 3389 idInPendingOrders = orderID in allOrdersIDs 3390 idInStopOrders = orderID in allStopOrdersIDs 3391 3392 if not (idInPendingOrders or idInStopOrders): 3393 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3394 continue 3395 3396 else: 3397 if idInPendingOrders: 3398 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3399 3400 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3401 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3402 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3403 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3404 3405 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3406 if self.moreDebug: 3407 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3408 3409 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3410 3411 else: 3412 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3413 3414 elif idInStopOrders: 3415 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3416 3417 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3418 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3419 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3420 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3421 3422 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3423 if self.moreDebug: 3424 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3425 3426 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3427 3428 else: 3429 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3430 3431 else: 3432 continue 3433 3434 def CloseAllOrders(self) -> None: 3435 """ 3436 Gets a list of open pending and stop orders and cancel it all. 3437 """ 3438 rawOrders = self.RequestPendingOrders() 3439 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3440 lenOrders = len(allOrdersIDs) 3441 3442 rawStopOrders = self.RequestStopOrders() 3443 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3444 lenSOrders = len(allStopOrdersIDs) 3445 3446 if lenOrders > 0 or lenSOrders > 0: 3447 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3448 3449 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3450 3451 else: 3452 uLogger.info("Orders not found, nothing to cancel.") 3453 3454 def CloseAll(self, *args) -> None: 3455 """ 3456 Close all available (not blocked) opened trades and orders. 3457 3458 Also, you can select one or more keywords case-insensitive: 3459 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3460 3461 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3462 """ 3463 overview = self.Overview(show=False) # get all open trades info 3464 3465 if len(args) == 0: 3466 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3467 self.CloseAllOrders() # close all pending and stop orders 3468 3469 for iType in TKS_INSTRUMENTS: 3470 if iType != "Currencies": 3471 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3472 3473 else: 3474 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3475 lowerArgs = [x.lower() for x in args] 3476 3477 if "orders" in lowerArgs: 3478 self.CloseAllOrders() # close all pending and stop orders 3479 3480 for iType in TKS_INSTRUMENTS: 3481 if iType.lower() in lowerArgs and iType != "Currencies": 3482 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3483 3484 @staticmethod 3485 def ParseOrderParameters(operation, **inputParameters): 3486 """ 3487 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3488 3489 :param operation: string "Buy" or "Sell". 3490 :param inputParameters: this is dict of strings that looks like this 3491 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3492 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3493 "prices" key: one or more prices to open limit-orders 3494 Counts of values in lots and prices lists must be equals! 3495 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3496 """ 3497 # TODO: update order grid work with api v2 3498 pass 3499 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3500 # 3501 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3502 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3503 # raise Exception("Incorrect value") 3504 # 3505 # if "l" in inputParameters.keys(): 3506 # inputParameters["lots"] = inputParameters.pop("l") 3507 # 3508 # if "p" in inputParameters.keys(): 3509 # inputParameters["prices"] = inputParameters.pop("p") 3510 # 3511 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3512 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3513 # raise Exception("Incorrect value") 3514 # 3515 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3516 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3517 # 3518 # if len(lots) != len(prices): 3519 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3520 # raise Exception("Incorrect value") 3521 # 3522 # uLogger.debug("Extracted parameters for orders:") 3523 # uLogger.debug("lots = {}".format(lots)) 3524 # uLogger.debug("prices = {}".format(prices)) 3525 # 3526 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3527 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3528 # uLogger.debug("Order parameters: {}".format(result)) 3529 # 3530 # return result 3531 3532 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3533 """ 3534 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3535 3536 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3537 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3538 """ 3539 result = False 3540 msg = "Instrument not defined!" 3541 3542 if portfolio is None or not portfolio: 3543 portfolio = self.Overview(show=False) 3544 3545 if self.ticker: 3546 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3547 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3548 3549 for iType in TKS_INSTRUMENTS: 3550 for instrument in portfolio["stat"][iType]: 3551 if instrument["ticker"] == self.ticker: 3552 result = True 3553 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3554 break 3555 3556 elif self.figi: 3557 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3558 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3559 3560 for iType in TKS_INSTRUMENTS: 3561 for instrument in portfolio["stat"][iType]: 3562 if instrument["figi"] == self.figi: 3563 result = True 3564 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3565 break 3566 3567 else: 3568 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3569 3570 uLogger.debug(msg) 3571 3572 return result 3573 3574 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3575 """ 3576 Returns instrument is in the user's portfolio if it presents there. 3577 Instrument must be defined by `ticker` (highly priority) or `figi`. 3578 3579 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3580 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3581 """ 3582 result = None 3583 msg = "Instrument not defined!" 3584 3585 if portfolio is None or not portfolio: 3586 portfolio = self.Overview(show=False) 3587 3588 if self.ticker: 3589 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3590 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3591 3592 for iType in TKS_INSTRUMENTS: 3593 for instrument in portfolio["stat"][iType]: 3594 if instrument["ticker"] == self.ticker: 3595 result = instrument 3596 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3597 break 3598 3599 elif self.figi: 3600 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3601 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3602 3603 for iType in TKS_INSTRUMENTS: 3604 for instrument in portfolio["stat"][iType]: 3605 if instrument["figi"] == self.figi: 3606 result = instrument 3607 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3608 break 3609 3610 else: 3611 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3612 3613 uLogger.debug(msg) 3614 3615 return result 3616 3617 def RequestLimits(self) -> dict: 3618 """ 3619 Method for obtaining the available funds for withdrawal for current `accountId`. 3620 3621 See also: 3622 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3623 - `OverviewLimits()` method 3624 3625 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3626 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3627 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3628 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3629 """ 3630 if self.accountId is None or not self.accountId: 3631 uLogger.error("Variable `accountId` must be defined for using this method!") 3632 raise Exception("Account ID required") 3633 3634 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3635 3636 self.body = str({"accountId": self.accountId}) 3637 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3638 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3639 3640 if self.moreDebug: 3641 uLogger.debug("Records about available funds for withdrawal successfully received") 3642 3643 return rawLimits 3644 3645 def OverviewLimits(self, show: bool = False) -> dict: 3646 """ 3647 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3648 3649 See also: `RequestLimits()`. 3650 3651 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3652 :return: dict with raw parsed data from server and some calculated statistics about it. 3653 """ 3654 if self.accountId is None or not self.accountId: 3655 uLogger.error("Variable `accountId` must be defined for using this method!") 3656 raise Exception("Account ID required") 3657 3658 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3659 3660 view = { 3661 "rawLimits": rawLimits, 3662 "limits": { # parsed data for every currency: 3663 "money": { # this is an array of portfolio currency positions 3664 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3665 }, 3666 "blocked": { # this is an array of blocked currency 3667 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3668 }, 3669 "blockedGuarantee": { # this is locked money under collateral for futures 3670 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3671 }, 3672 }, 3673 } 3674 3675 # --- Prepare text table with limits in human-readable format: 3676 if show: 3677 info = [ 3678 "# Withdrawal limits\n\n", 3679 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3680 "* **Account ID:** [{}]\n".format(self.accountId), 3681 ] 3682 3683 if view["limits"]["money"]: 3684 info.extend([ 3685 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3686 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3687 ]) 3688 3689 else: 3690 info.append("\nNo withdrawal limits\n") 3691 3692 for curr in view["limits"]["money"].keys(): 3693 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3694 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3695 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3696 3697 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3698 "[{}]".format(curr), 3699 "{:.2f}".format(view["limits"]["money"][curr]), 3700 "{:.2f}".format(availableMoney), 3701 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3702 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3703 ) 3704 3705 if curr == "rub": 3706 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3707 3708 else: 3709 info.append(infoStr) 3710 3711 infoText = "".join(info) 3712 3713 uLogger.info(infoText) 3714 3715 if self.withdrawalLimitsFile: 3716 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3717 fH.write(infoText) 3718 3719 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3720 3721 return view 3722 3723 def RequestAccounts(self) -> dict: 3724 """ 3725 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3726 3727 See also: 3728 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3729 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3730 - `OverviewUserInfo()` method 3731 3732 :return: dict with raw data from server that contains accounts info. Example of dict: 3733 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3734 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3735 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3736 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3737 """ 3738 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3739 3740 self.body = str({}) 3741 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3742 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3743 3744 if self.moreDebug: 3745 uLogger.debug("Records about available accounts successfully received") 3746 3747 return rawAccounts 3748 3749 def RequestUserInfo(self) -> dict: 3750 """ 3751 Method for requesting common user's information. 3752 3753 See also: 3754 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3755 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3756 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3757 - `OverviewUserInfo()` method 3758 3759 :return: dict with raw data from server that contains user's information. Example of dict: 3760 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3761 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3762 """ 3763 uLogger.debug("Requesting common user's information. Wait, please...") 3764 3765 self.body = str({}) 3766 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3767 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3768 3769 if self.moreDebug: 3770 uLogger.debug("Records about current user successfully received") 3771 3772 return rawUserInfo 3773 3774 def RequestMarginStatus(self, accountId: str = None) -> dict: 3775 """ 3776 Method for requesting margin calculation for defined account ID. 3777 3778 See also: 3779 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3780 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3781 - `OverviewUserInfo()` method 3782 3783 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3784 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3785 Example of responses: 3786 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3787 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3788 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3789 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3790 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3791 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3792 """ 3793 if accountId is None or not accountId: 3794 if self.accountId is None or not self.accountId: 3795 uLogger.error("Variable `accountId` must be defined for using this method!") 3796 raise Exception("Account ID required") 3797 3798 else: 3799 accountId = self.accountId # use `self.accountId` (main ID) by default 3800 3801 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3802 3803 self.body = str({"accountId": accountId}) 3804 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3805 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3806 3807 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3808 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3809 rawMargin = {} 3810 3811 else: 3812 if self.moreDebug: 3813 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3814 3815 return rawMargin 3816 3817 def RequestTariffLimits(self) -> dict: 3818 """ 3819 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3820 3821 See also: 3822 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3823 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3824 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3825 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3826 - `OverviewUserInfo()` method 3827 3828 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3829 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3830 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3831 """ 3832 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3833 3834 self.body = str({}) 3835 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3836 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3837 3838 if self.moreDebug: 3839 uLogger.debug("Records with limits of current tariff successfully received") 3840 3841 return rawTariffLimits 3842 3843 def RequestBondCoupons(self, iJSON: dict) -> dict: 3844 """ 3845 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3846 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3847 All dates are in UTC timezone. 3848 3849 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3850 Documentation: 3851 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3852 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3853 3854 See also: `ExtendBondsData()`. 3855 3856 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3857 If raw iJSON is not data of bond then server returns an error [400] with message: 3858 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3859 :return: dictionary with bond payment calendar. Response example 3860 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3861 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3862 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3863 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3864 """ 3865 if iJSON["figi"] is None or not iJSON["figi"]: 3866 uLogger.error("FIGI must be defined for using this method!") 3867 raise Exception("FIGI required") 3868 3869 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3870 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3871 3872 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3873 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3874 self.figi, 3875 startDate, 3876 endDate, 3877 )) 3878 3879 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3880 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3881 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3882 3883 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3884 uLogger.warning("Instrument type is not bond!") 3885 3886 else: 3887 if self.moreDebug: 3888 uLogger.debug("Records about bond payment calendar successfully received") 3889 3890 return calendar 3891 3892 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3893 """ 3894 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3895 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3896 coupon yields, current yields and some statistics etc. 3897 3898 WARNING! This is too long operation if a lot of bonds requested from broker server. 3899 3900 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3901 3902 :param instruments: list of strings with tickers or FIGIs. 3903 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3904 for further used by data scientists or stock analytics. 3905 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3906 In XLSX-file and Pandas DataFrame fields mean: 3907 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3908 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3909 """ 3910 if instruments is None or not instruments: 3911 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3912 raise Exception("Ticker or FIGI required") 3913 3914 if isinstance(instruments, str): 3915 instruments = [instruments] 3916 3917 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3918 3919 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3920 3921 iCount = len(uniqueInstruments) 3922 tooLong = iCount >= 20 3923 if tooLong: 3924 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3925 3926 bonds = None 3927 for i, self.figi in enumerate(uniqueInstruments): 3928 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3929 3930 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3931 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3932 rawBond = self.SearchByFIGI(requestPrice=True) 3933 3934 # Widen raw data with UTC current time (iData["actualDateTime"]): 3935 actualDate = datetime.now(tzutc()) 3936 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3937 3938 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3939 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3940 3941 # Replace some values with human-readable: 3942 iData["nominalCurrency"] = iData["nominal"]["currency"] 3943 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3944 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3945 iData["aciCurrency"] = iData["aciValue"]["currency"] 3946 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3947 iData["issueSize"] = int(iData["issueSize"]) 3948 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3949 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3950 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3951 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3952 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3953 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3954 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3955 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3956 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3957 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3958 3959 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3960 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3961 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3962 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3963 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3964 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3965 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3966 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3967 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3968 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3969 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3970 3971 # Widen raw data with calendar data from `rawCalendar` values: 3972 calendarData = [] 3973 if "events" in iData["rawCalendar"].keys(): 3974 for item in iData["rawCalendar"]["events"]: 3975 calendarData.append({ 3976 "couponDate": item["couponDate"], 3977 "couponNumber": int(item["couponNumber"]), 3978 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3979 "payCurrency": item["payOneBond"]["currency"], 3980 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3981 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3982 "couponStartDate": item["couponStartDate"], 3983 "couponEndDate": item["couponEndDate"], 3984 "couponPeriod": item["couponPeriod"], 3985 }) 3986 3987 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3988 if "maturityDate" not in iData.keys(): 3989 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3990 3991 # Widen raw data with Coupon Rate. 3992 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3993 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3994 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3995 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3996 3997 # Widen raw data with Yield to Maturity (YTM) on current date. 3998 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3999 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4000 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4001 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4002 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4003 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4004 4005 iData["calendar"] = calendarData # adds calendar at the end 4006 4007 # Remove not used data: 4008 iData.pop("uid") 4009 iData.pop("positionUid") 4010 iData.pop("currentPrice") 4011 iData.pop("rawCalendar") 4012 4013 colNames = list(iData.keys()) 4014 if bonds is None: 4015 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4016 4017 else: 4018 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4019 4020 else: 4021 uLogger.warning("Instrument is not a bond!") 4022 4023 processed = round(100 * (i + 1) / iCount, 1) 4024 if tooLong and processed % 5 == 0: 4025 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4026 4027 else: 4028 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4029 4030 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4031 4032 # Saving bonds from Pandas DataFrame to XLSX sheet: 4033 if xlsx and self.bondsXLSXFile: 4034 with pd.ExcelWriter( 4035 path=self.bondsXLSXFile, 4036 date_format=TKS_DATE_FORMAT, 4037 datetime_format=TKS_DATE_TIME_FORMAT, 4038 mode="w", 4039 ) as writer: 4040 bonds.to_excel( 4041 writer, 4042 sheet_name="Extended bonds data", 4043 index=True, 4044 encoding="UTF-8", 4045 freeze_panes=(1, 1), 4046 ) # saving as XLSX-file with freeze first row and column as headers 4047 4048 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4049 4050 return bonds 4051 4052 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4053 """ 4054 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4055 4056 WARNING! This is too long operation if a lot of bonds requested from broker server. 4057 4058 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4059 4060 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4061 extended information about bonds: main info, current prices, bond payment calendar, 4062 coupon yields, current yields and some statistics etc. 4063 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4064 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4065 for further used by data scientists or stock analytics. 4066 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4067 """ 4068 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4069 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4070 4071 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4072 4073 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4074 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4075 calendar = None 4076 for bond in extBonds.iterrows(): 4077 for item in bond[1]["calendar"]: 4078 cData = { 4079 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4080 "couponDate": item["couponDate"], 4081 "figi": bond[1]["figi"], 4082 "ticker": bond[1]["ticker"], 4083 "name": bond[1]["name"], 4084 "couponNumber": item["couponNumber"], 4085 "payOneBond": item["payOneBond"], 4086 "payCurrency": item["payCurrency"], 4087 "couponType": item["couponType"], 4088 "couponPeriod": item["couponPeriod"], 4089 "fixDate": item["fixDate"], 4090 "couponStartDate": item["couponStartDate"], 4091 "couponEndDate": item["couponEndDate"], 4092 } 4093 4094 if calendar is None: 4095 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4096 4097 else: 4098 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4099 4100 if calendar is not None: 4101 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4102 4103 # Saving calendar from Pandas DataFrame to XLSX sheet: 4104 if xlsx: 4105 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4106 4107 with pd.ExcelWriter( 4108 path=xlsxCalendarFile, 4109 date_format=TKS_DATE_FORMAT, 4110 datetime_format=TKS_DATE_TIME_FORMAT, 4111 mode="w", 4112 ) as writer: 4113 humanReadable = calendar.copy(deep=True) 4114 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4115 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4116 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4117 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4118 humanReadable.columns = colNames # human-readable column names 4119 4120 humanReadable.to_excel( 4121 writer, 4122 sheet_name="Bond payments calendar", 4123 index=False, 4124 encoding="UTF-8", 4125 freeze_panes=(1, 2), 4126 ) # saving as XLSX-file with freeze first row and column as headers 4127 4128 del humanReadable # release df in memory 4129 4130 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4131 4132 return calendar 4133 4134 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4135 """ 4136 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4137 Also, creates Markdown file with calendar data, `calendar.md` by default. 4138 4139 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4140 4141 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4142 extended information about bonds: main info, current prices, bond payment calendar, 4143 coupon yields, current yields and some statistics etc. 4144 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4145 :param show: if `True` then also printing bonds payment calendar to the console, 4146 otherwise save to file `calendarFile` only. `False` by default. 4147 :return: multilines text in Markdown format with bonds payment calendar as a table. 4148 """ 4149 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4150 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4151 4152 infoText = "# Bond payments calendar\n\n" 4153 4154 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4155 4156 if not (calendar is None or calendar.empty): 4157 splitLine = "| | | | | | | | | |\n" 4158 4159 info = [ 4160 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4161 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4162 ] 4163 4164 newMonth = False 4165 notOneBond = calendar["figi"].nunique() > 1 4166 for i, bond in enumerate(calendar.iterrows()): 4167 if newMonth and notOneBond: 4168 info.append(splitLine) 4169 4170 info.append( 4171 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4172 " √" if bond[1]["paid"] else " —", 4173 bond[1]["couponDate"].split("T")[0], 4174 bond[1]["figi"], 4175 bond[1]["ticker"], 4176 bond[1]["couponNumber"], 4177 "{} {}".format( 4178 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4179 bond[1]["payCurrency"], 4180 ), 4181 bond[1]["couponType"], 4182 bond[1]["couponPeriod"], 4183 bond[1]["fixDate"].split("T")[0], 4184 ) 4185 ) 4186 4187 if i < len(calendar.values) - 1: 4188 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4189 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4190 newMonth = False if curDate.month == nextDate.month else True 4191 4192 else: 4193 newMonth = False 4194 4195 infoText += "".join(info) 4196 4197 if show: 4198 uLogger.info("{}".format(infoText)) 4199 4200 if self.calendarFile is not None: 4201 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4202 fH.write(infoText) 4203 4204 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4205 4206 else: 4207 infoText += "No data\n" 4208 4209 return infoText 4210 4211 def OverviewAccounts(self, show: bool = False) -> dict: 4212 """ 4213 Method for parsing and show simple table with all available user accounts. 4214 4215 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4216 4217 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4218 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4219 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4220 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4221 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4222 "closed": "—", "access": "Full access" }, ...}}` 4223 """ 4224 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4225 4226 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4227 accounts = { 4228 item["id"]: { 4229 "type": TKS_ACCOUNT_TYPES[item["type"]], 4230 "name": item["name"], 4231 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4232 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4233 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4234 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4235 } for item in rawAccounts["accounts"] 4236 } 4237 4238 # Raw and parsed data with some fields replaced in "stat" section: 4239 view = { 4240 "rawAccounts": rawAccounts, 4241 "stat": accounts, 4242 } 4243 4244 # --- Prepare simple text table with only accounts data in human-readable format: 4245 if show: 4246 info = [ 4247 "# User accounts\n\n", 4248 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4249 "| Account ID | Type | Status | Name |\n", 4250 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4251 ] 4252 4253 for account in view["stat"].keys(): 4254 info.extend([ 4255 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4256 account, 4257 view["stat"][account]["type"], 4258 view["stat"][account]["status"], 4259 view["stat"][account]["name"], 4260 ) 4261 ]) 4262 4263 infoText = "".join(info) 4264 4265 uLogger.info(infoText) 4266 4267 if self.userAccountsFile: 4268 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4269 fH.write(infoText) 4270 4271 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4272 4273 return view 4274 4275 def OverviewUserInfo(self, show: bool = False) -> dict: 4276 """ 4277 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4278 4279 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4280 4281 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4282 :return: dict with raw parsed data from server and some calculated statistics about it. 4283 """ 4284 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4285 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4286 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4287 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4288 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4289 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4290 4291 # This is dict with parsed common user data: 4292 userInfo = { 4293 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4294 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4295 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4296 "tariff": rawUserInfo["tariff"], 4297 } 4298 4299 # This is an array of dict with parsed margin statuses for every account IDs: 4300 margins = {} 4301 for accountId in accounts.keys(): 4302 if rawMargins[accountId]: 4303 margins[accountId] = { 4304 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4305 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4306 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4307 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4308 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4309 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4310 } 4311 4312 else: 4313 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4314 4315 unary = {} # unary-connection limits 4316 for item in rawTariffLimits["unaryLimits"]: 4317 if item["limitPerMinute"] in unary.keys(): 4318 unary[item["limitPerMinute"]].extend(item["methods"]) 4319 4320 else: 4321 unary[item["limitPerMinute"]] = item["methods"] 4322 4323 stream = {} # stream-connection limits 4324 for item in rawTariffLimits["streamLimits"]: 4325 if item["limit"] in stream.keys(): 4326 stream[item["limit"]].extend(item["streams"]) 4327 4328 else: 4329 stream[item["limit"]] = item["streams"] 4330 4331 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4332 limits = { 4333 "unary": unary, 4334 "stream": stream, 4335 } 4336 4337 # Raw and parsed data as an output result: 4338 view = { 4339 "rawUserInfo": rawUserInfo, 4340 "rawAccounts": rawAccounts, 4341 "rawMargins": rawMargins, 4342 "rawTariffLimits": rawTariffLimits, 4343 "stat": { 4344 "userInfo": userInfo, 4345 "accounts": accounts, 4346 "margins": margins, 4347 "limits": limits, 4348 }, 4349 } 4350 4351 # --- Prepare text table with user information in human-readable format: 4352 if show: 4353 info = [ 4354 "# Full user information\n\n", 4355 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4356 "## Common information\n\n", 4357 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4358 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4359 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4360 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4361 "\n## User accounts\n\n", 4362 ] 4363 4364 for account in view["stat"]["accounts"].keys(): 4365 info.extend([ 4366 "### ID: [{}]\n\n".format(account), 4367 "| Parameters | Values |\n", 4368 "|----------------------|--------------------------------------------------------------|\n", 4369 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4370 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4371 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4372 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4373 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4374 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4375 ]) 4376 4377 if margins[account]: 4378 info.extend([ 4379 "| Margin status: | Enabled |\n", 4380 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4381 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4382 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4383 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4384 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4385 ]) 4386 4387 else: 4388 info.append("| Margin status: | Disabled |\n\n") 4389 4390 info.extend([ 4391 "\n## Current user tariff limits\n", 4392 "\nSee also:\n", 4393 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4394 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4395 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4396 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4397 "\n### Unary limits\n", 4398 ]) 4399 4400 if unary: 4401 for key, values in sorted(unary.items()): 4402 info.append("\n* Max requests per minute: {}\n".format(key)) 4403 4404 for value in values: 4405 info.append(" - {}\n".format(value)) 4406 4407 else: 4408 info.append("\nNot available\n") 4409 4410 info.append("\n### Stream limits\n") 4411 4412 if stream: 4413 for key, values in sorted(stream.items()): 4414 info.append("\n* Max stream connections: {}\n".format(key)) 4415 4416 for value in values: 4417 info.append(" - {}\n".format(value)) 4418 4419 else: 4420 info.append("\nNot available\n") 4421 4422 infoText = "".join(info) 4423 4424 uLogger.info(infoText) 4425 4426 if self.userInfoFile: 4427 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4428 fH.write(infoText) 4429 4430 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4431 4432 return view 4433 4434 4435class Args: 4436 """ 4437 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4438 """ 4439 def __init__(self, **kwargs): 4440 self.__dict__.update(kwargs) 4441 4442 def __getattr__(self, item): 4443 return None 4444 4445 4446def ParseArgs(): 4447 """This function get and parse command line keys.""" 4448 parser = ArgumentParser() # command-line string parser 4449 4450 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4451 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4452 4453 # --- options: 4454 4455 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4456 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4457 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4458 4459 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4460 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4461 4462 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4463 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4464 4465 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4466 4467 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4468 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4469 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4470 4471 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4472 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4473 4474 # --- commands: 4475 4476 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4477 4478 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4479 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4480 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4481 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4482 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4483 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4484 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4485 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4486 4487 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4488 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4489 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4490 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4491 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4492 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4493 4494 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4495 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4496 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4497 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4498 4499 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4500 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4501 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4502 4503 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4504 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4505 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4506 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4507 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4508 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4509 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4510 4511 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4512 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4513 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4514 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4515 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4516 4517 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4518 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4519 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4520 4521 cmdArgs = parser.parse_args() 4522 return cmdArgs 4523 4524 4525def Main(**kwargs): 4526 """ 4527 Main function for work with TKSBrokerAPI in the console. 4528 4529 See examples: 4530 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4531 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4532 """ 4533 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4534 4535 if args.debug_level: 4536 uLogger.level = 10 # always debug level by default 4537 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4538 4539 exitCode = 0 4540 start = datetime.now(tzutc()) 4541 uLogger.debug("=-" * 50) 4542 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4543 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4544 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4545 )) 4546 4547 # trying to calculate full current version: 4548 buildVersion = __version__ 4549 try: 4550 v = version("tksbrokerapi") 4551 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4552 4553 except Exception: 4554 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4555 4556 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4557 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4558 4559 try: 4560 if args.version: 4561 print("TKSBrokerAPI {}".format(buildVersion)) 4562 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4563 4564 else: 4565 # Init class for trading with Tinkoff Broker: 4566 trader = TinkoffBrokerServer( 4567 token=args.token, 4568 accountId=args.account_id, 4569 useCache=not args.no_cache, 4570 ) 4571 4572 # --- set some options: 4573 4574 if args.more: 4575 trader.moreDebug = True 4576 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4577 4578 if args.ticker: 4579 if args.ticker in trader.aliasesKeys: 4580 trader.ticker = trader.aliases[args.ticker] # Replace some tickers with its aliases 4581 4582 else: 4583 trader.ticker = args.ticker 4584 4585 if args.figi: 4586 trader.figi = args.figi 4587 4588 if args.depth is not None: 4589 trader.depth = args.depth 4590 4591 # --- do one command: 4592 4593 if args.list: 4594 if args.output is not None: 4595 trader.instrumentsFile = args.output 4596 4597 trader.ShowInstrumentsInfo(show=True) 4598 4599 elif args.list_xlsx: 4600 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4601 4602 elif args.bonds_xlsx is not None: 4603 if args.output is not None: 4604 trader.bondsXLSXFile = args.output 4605 4606 if len(args.bonds_xlsx) == 0: 4607 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4608 4609 else: 4610 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4611 4612 elif args.search: 4613 if args.output is not None: 4614 trader.searchResultsFile = args.output 4615 4616 trader.SearchInstruments(pattern=args.search[0], show=True) 4617 4618 elif args.info: 4619 if not (args.ticker or args.figi): 4620 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4621 raise Exception("Ticker or FIGI required") 4622 4623 if args.output is not None: 4624 trader.infoFile = args.output 4625 4626 if args.ticker: 4627 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4628 4629 else: 4630 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4631 4632 elif args.calendar is not None: 4633 if args.output is not None: 4634 trader.calendarFile = args.output 4635 4636 if len(args.calendar) == 0: 4637 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4638 4639 else: 4640 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4641 4642 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4643 4644 elif args.price: 4645 if not (args.ticker or args.figi): 4646 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4647 raise Exception("Ticker or FIGI required") 4648 4649 trader.GetCurrentPrices(show=True) 4650 4651 elif args.prices is not None: 4652 if args.output is not None: 4653 trader.pricesFile = args.output 4654 4655 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4656 4657 elif args.overview: 4658 if args.output is not None: 4659 trader.overviewFile = args.output 4660 4661 trader.Overview(show=True, details="full") 4662 4663 elif args.overview_digest: 4664 if args.output is not None: 4665 trader.overviewDigestFile = args.output 4666 4667 trader.Overview(show=True, details="digest") 4668 4669 elif args.overview_positions: 4670 if args.output is not None: 4671 trader.overviewPositionsFile = args.output 4672 4673 trader.Overview(show=True, details="positions") 4674 4675 elif args.overview_orders: 4676 if args.output is not None: 4677 trader.overviewOrdersFile = args.output 4678 4679 trader.Overview(show=True, details="orders") 4680 4681 elif args.overview_analytics: 4682 if args.output is not None: 4683 trader.overviewAnalyticsFile = args.output 4684 4685 trader.Overview(show=True, details="analytics") 4686 4687 elif args.overview_calendar: 4688 if args.output is not None: 4689 trader.overviewAnalyticsFile = args.output 4690 4691 trader.Overview(show=True, details="calendar") 4692 4693 elif args.deals is not None: 4694 if args.output is not None: 4695 trader.reportFile = args.output 4696 4697 if 0 <= len(args.deals) < 3: 4698 trader.Deals( 4699 start=args.deals[0] if len(args.deals) >= 1 else None, 4700 end=args.deals[1] if len(args.deals) == 2 else None, 4701 show=True, # Always show deals report in console 4702 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4703 ) 4704 4705 else: 4706 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4707 raise Exception("Incorrect value") 4708 4709 elif args.history is not None: 4710 if args.output is not None: 4711 trader.historyFile = args.output 4712 4713 if 0 <= len(args.history) < 3: 4714 dataReceived = trader.History( 4715 start=args.history[0] if len(args.history) >= 1 else None, 4716 end=args.history[1] if len(args.history) == 2 else None, 4717 interval="hour" if args.interval is None or not args.interval else args.interval, 4718 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4719 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4720 show=True, # shows all downloaded candles in console 4721 ) 4722 4723 if args.render_chart is not None and dataReceived is not None: 4724 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4725 4726 trader.ShowHistoryChart( 4727 candles=dataReceived, 4728 interact=iChart, 4729 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4730 ) 4731 4732 else: 4733 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4734 raise Exception("Incorrect value") 4735 4736 elif args.load_history is not None: 4737 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4738 4739 if args.render_chart is not None and histData is not None: 4740 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4741 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4742 4743 trader.ShowHistoryChart( 4744 candles=histData, 4745 interact=iChart, 4746 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4747 ) 4748 4749 elif args.trade is not None: 4750 if 1 <= len(args.trade) <= 5: 4751 trader.Trade( 4752 operation=args.trade[0], 4753 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4754 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4755 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4756 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4757 ) 4758 4759 else: 4760 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4761 4762 elif args.buy is not None: 4763 if 0 <= len(args.buy) <= 4: 4764 trader.Buy( 4765 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4766 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4767 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4768 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4769 ) 4770 4771 else: 4772 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4773 4774 elif args.sell is not None: 4775 if 0 <= len(args.sell) <= 4: 4776 trader.Sell( 4777 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4778 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4779 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4780 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4781 ) 4782 4783 else: 4784 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4785 4786 elif args.order: 4787 if 4 <= len(args.order) <= 7: 4788 trader.Order( 4789 operation=args.order[0], 4790 orderType=args.order[1], 4791 lots=int(args.order[2]), 4792 targetPrice=float(args.order[3]), 4793 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4794 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4795 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4796 ) 4797 4798 else: 4799 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4800 4801 elif args.buy_limit: 4802 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4803 4804 elif args.sell_limit: 4805 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4806 4807 elif args.buy_stop: 4808 if 2 <= len(args.buy_stop) <= 7: 4809 trader.BuyStop( 4810 lots=int(args.buy_stop[0]), 4811 targetPrice=float(args.buy_stop[1]), 4812 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4813 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4814 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4815 ) 4816 4817 else: 4818 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4819 4820 elif args.sell_stop: 4821 if 2 <= len(args.sell_stop) <= 7: 4822 trader.SellStop( 4823 lots=int(args.sell_stop[0]), 4824 targetPrice=float(args.sell_stop[1]), 4825 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4826 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4827 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4828 ) 4829 4830 else: 4831 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4832 4833 # elif args.buy_order_grid is not None: 4834 # # update order grid work with api v2 4835 # if len(args.buy_order_grid) == 2: 4836 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4837 # 4838 # for order in orderParams: 4839 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4840 # 4841 # else: 4842 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4843 # 4844 # elif args.sell_order_grid is not None: 4845 # # update order grid work with api v2 4846 # if len(args.sell_order_grid) >= 2: 4847 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4848 # 4849 # for order in orderParams: 4850 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4851 # 4852 # else: 4853 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4854 4855 elif args.close_order is not None: 4856 trader.CloseOrders(args.close_order) # close only one order 4857 4858 elif args.close_orders is not None: 4859 trader.CloseOrders(args.close_orders) # close list of orders 4860 4861 elif args.close_trade: 4862 if not (args.ticker or args.figi): 4863 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4864 raise Exception("Ticker or FIGI required") 4865 4866 if args.ticker: 4867 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4868 4869 else: 4870 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4871 4872 elif args.close_trades is not None: 4873 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4874 4875 elif args.close_all is not None: 4876 trader.CloseAll(*args.close_all) 4877 4878 elif args.limits: 4879 if args.output is not None: 4880 trader.withdrawalLimitsFile = args.output 4881 4882 trader.OverviewLimits(show=True) 4883 4884 elif args.user_info: 4885 if args.output is not None: 4886 trader.userInfoFile = args.output 4887 4888 trader.OverviewUserInfo(show=True) 4889 4890 elif args.account: 4891 if args.output is not None: 4892 trader.userAccountsFile = args.output 4893 4894 trader.OverviewAccounts(show=True) 4895 4896 else: 4897 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4898 raise Exception("There is no command to execute") 4899 4900 except Exception: 4901 trace = tb.format_exc() 4902 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4903 if e in trace: 4904 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4905 break 4906 4907 uLogger.debug(trace) 4908 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4909 exitCode = 255 # an error occurred, must be open a ticket for this issue 4910 4911 finally: 4912 finish = datetime.now(tzutc()) 4913 4914 if exitCode == 0: 4915 if args.more: 4916 uLogger.debug("All operations were finished success (summary code is 0).") 4917 4918 else: 4919 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4920 os.path.abspath(uLog.defaultLogFile), exitCode, 4921 )) 4922 4923 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4924 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4925 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4926 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4927 )) 4928 uLogger.debug("=-" * 50) 4929 4930 if not kwargs: 4931 sys.exit(exitCode) 4932 4933 else: 4934 return exitCode 4935 4936 4937if __name__ == "__main__": 4938 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` — how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 — how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.overviewBondsCalendarFile = "overview-calendar.md" 385 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 386 387 See also: `Overview()` with parameter `details="calendar"`. 388 """ 389 390 self.reportFile = "deals.md" 391 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 392 393 See also: `Deals()`. 394 """ 395 396 self.withdrawalLimitsFile = "limits.md" 397 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 398 399 See also: `OverviewLimits()` and `RequestLimits()`. 400 """ 401 402 self.userInfoFile = "user-info.md" 403 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 404 405 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 406 """ 407 408 self.userAccountsFile = "accounts.md" 409 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 410 411 See also: `OverviewAccounts()`, `RequestAccounts()`. 412 """ 413 414 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 415 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 416 417 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 418 419 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 420 """ 421 422 self.iList = None # init iList for raw instruments data 423 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 424 425 See also: `Listing()`, `DumpInstruments()`. 426 """ 427 428 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 429 if useCache: 430 if os.path.exists(self.iListDumpFile): 431 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 432 curTime = datetime.now(tzutc()) 433 434 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 435 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 436 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 438 439 else: 440 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 441 442 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 443 os.path.abspath(self.iListDumpFile), 444 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 445 )) 446 447 else: 448 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 449 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 450 451 else: 452 self.iList = self.Listing() # request new raw instruments data from broker server 453 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 454 455 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 456 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 457 458 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 459 """ 460 461 def _ParseJSON(self, rawData="{}") -> dict: 462 """ 463 Parse JSON from response string. 464 465 :param rawData: this is a string with JSON-formatted text. 466 :return: JSON (dictionary), parsed from server response string. 467 """ 468 responseJSON = json.loads(rawData) if rawData else {} 469 470 if self.moreDebug: 471 uLogger.debug("JSON formatted raw body data of response:\n{}".format(json.dumps(responseJSON, indent=4))) 472 473 return responseJSON 474 475 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 476 """ 477 Send GET or POST request to broker server and receive JSON object. 478 479 self.header: must be defining with dictionary of headers. 480 self.body: if define then used as request body. None by default. 481 self.timeout: global request timeout, 15 seconds by default. 482 :param url: url with REST request. 483 :param reqType: send "GET" or "POST" request. "GET" by default. 484 :param retry: how many times retry after first request if an 5xx server errors occurred. 485 :param pause: sleep time in seconds between retries. 486 :return: response JSON (dictionary) from broker. 487 """ 488 if reqType not in ("GET", "POST"): 489 uLogger.error("You can define request type: 'GET' or 'POST'!") 490 raise Exception("Incorrect value") 491 492 if self.moreDebug: 493 uLogger.debug("Request parameters:") 494 uLogger.debug(" - REST API URL: {}".format(url)) 495 uLogger.debug(" - request type: {}".format(reqType)) 496 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 497 uLogger.debug(" - body:\n{}".format(self.body)) 498 499 # fast hack to avoid all operations with some tickers/FIGI 500 responseJSON = {} 501 oK = True 502 for item in self.exclude: 503 if item in url: 504 if self.moreDebug: 505 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 506 507 oK = False 508 break 509 510 if oK: 511 counter = 0 512 response = None 513 errMsg = "" 514 515 while not response and counter <= retry: 516 if reqType == "GET": 517 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if reqType == "POST": 520 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 521 522 if self.moreDebug: 523 uLogger.debug("Response:") 524 uLogger.debug(" - status code: {}".format(response.status_code)) 525 uLogger.debug(" - reason: {}".format(response.reason)) 526 uLogger.debug(" - body length: {}".format(len(response.text))) 527 uLogger.debug(" - headers:\n{}".format(response.headers)) 528 529 # Server returns some headers: 530 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 531 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 532 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 533 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 534 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 535 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 536 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 537 sleep(rateLimitWait) 538 539 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 540 if 400 <= response.status_code < 500: 541 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 542 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 543 counter = retry + 1 544 545 if 500 <= response.status_code < 600: 546 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 547 uLogger.debug(" - not oK, {}".format(errMsg)) 548 counter += 1 549 550 if counter <= retry: 551 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 552 sleep(pause) 553 554 responseJSON = self._ParseJSON(rawData=response.text) 555 556 if errMsg: 557 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 558 uLogger.error(" - not oK, {}".format(errMsg)) 559 560 return responseJSON 561 562 def _IUpdater(self, iType: str) -> tuple: 563 """ 564 Request instrument by type from server. See available API methods for instruments: 565 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 566 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 567 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 568 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 569 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 570 571 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 572 :return: tuple with iType name and list of available instruments of current type for defined user token. 573 """ 574 result = [] 575 576 if iType in TKS_INSTRUMENTS: 577 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 578 579 # all instruments have the same body in API v2 requests: 580 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 581 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 582 result = self.SendAPIRequest(instrumentURL, reqType="POST")["instruments"] 583 584 return iType, result 585 586 def _IWrapper(self, kwargs): 587 """ 588 Wrapper runs instrument's update method `_IUpdater()`. 589 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 590 """ 591 return self._IUpdater(**kwargs) 592 593 def Listing(self) -> dict: 594 """ 595 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 596 597 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 598 """ 599 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 600 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 601 602 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 603 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 604 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 605 606 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 607 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 608 poolUpdater.close() 609 610 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 611 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 612 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 613 614 # calculate minimum price increment (step) for all instruments and set up instrument's type: 615 for iType in iList.keys(): 616 for ticker in iList[iType]: 617 iList[iType][ticker]["type"] = iType 618 619 if "minPriceIncrement" in iList[iType][ticker].keys(): 620 iList[iType][ticker]["step"] = NanoToFloat( 621 iList[iType][ticker]["minPriceIncrement"]["units"], 622 iList[iType][ticker]["minPriceIncrement"]["nano"], 623 ) 624 625 else: 626 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 627 628 return iList 629 630 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 631 """ 632 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 633 634 See also: `DumpInstruments()`, `Listing()`. 635 636 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 637 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 638 """ 639 if self.iListDumpFile is None or not self.iListDumpFile: 640 uLogger.error("Output name of dump file must be defined!") 641 raise Exception("Filename required") 642 643 if not self.iList or forceUpdate: 644 self.iList = self.Listing() 645 646 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 647 648 # Save as XLSX with separated sheets for every type of instruments: 649 with pd.ExcelWriter( 650 path=xlsxDumpFile, 651 date_format=TKS_DATE_FORMAT, 652 datetime_format=TKS_DATE_TIME_FORMAT, 653 mode="w", 654 ) as writer: 655 for iType in TKS_INSTRUMENTS: 656 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 657 df = df[sorted(df)] # sorted by column names 658 df = df.applymap( 659 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 660 na_action="ignore", 661 ) # converting numbers from nano-type to float in every cell 662 df.to_excel( 663 writer, 664 sheet_name=iType, 665 encoding="UTF-8", 666 freeze_panes=(1, 1), 667 ) # saving as XLSX-file with freeze first row and column as headers 668 669 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 670 671 def DumpInstruments(self, forceUpdate: bool = True) -> str: 672 """ 673 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 674 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 675 676 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 677 678 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 679 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 680 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 681 """ 682 if self.iListDumpFile is None or not self.iListDumpFile: 683 uLogger.error("Output name of dump file must be defined!") 684 raise Exception("Filename required") 685 686 if not self.iList or forceUpdate: 687 self.iList = self.Listing() 688 689 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 690 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 691 fH.write(jsonDump) 692 693 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 694 695 return jsonDump 696 697 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 698 """ 699 Show information about one instrument defined by json data and prints it in Markdown format. 700 701 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 702 703 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 704 :param show: if `True` then also printing information about instrument and its current price. 705 :return: multilines text in Markdown format with information about one instrument. 706 """ 707 splitLine = "| | |\n" 708 infoText = "" 709 710 if iJSON is not None and iJSON and isinstance(iJSON, dict): 711 info = [ 712 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 713 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 714 "| Parameters | Values |\n", 715 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 716 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 717 "| Full name: | {:<54} |\n".format(iJSON["name"]), 718 ] 719 720 if "sector" in iJSON.keys() and iJSON["sector"]: 721 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 722 723 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 724 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 725 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 726 ))) 727 728 info.extend([ 729 splitLine, 730 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 731 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 732 ]) 733 734 if "isin" in iJSON.keys() and iJSON["isin"]: 735 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 736 737 if "classCode" in iJSON.keys(): 738 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 739 740 info.extend([ 741 splitLine, 742 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 743 splitLine, 744 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 745 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 746 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 747 ]) 748 749 if iJSON["figi"]: 750 self.figi = iJSON["figi"] 751 iJSON = iJSON | self.RequestTradingStatus() 752 753 info.extend([ 754 splitLine, 755 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 756 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 757 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 758 ]) 759 760 info.append(splitLine) 761 762 if "type" in iJSON.keys() and iJSON["type"]: 763 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 764 765 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 766 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 767 768 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 769 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 770 771 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 772 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 773 774 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 775 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 776 777 if "focusType" in iJSON.keys() and iJSON["focusType"]: 778 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 779 780 if "assetType" in iJSON.keys() and iJSON["assetType"]: 781 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 782 783 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 784 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 785 786 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 787 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 788 789 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 790 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 791 792 if "currency" in iJSON.keys(): 793 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 794 795 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 796 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 797 798 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 799 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 802 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 803 804 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 805 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 806 807 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 808 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 809 810 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 811 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 812 813 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 814 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 815 816 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 817 info.append("| Perpetual bond: | Yes |\n") 818 819 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 820 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 821 822 iExt = None 823 if iJSON["type"] == "Bonds": 824 info.extend([ 825 splitLine, 826 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 827 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 828 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 829 iJSON["nominal"]["currency"], 830 )), 831 ]) 832 833 if "floatingCouponFlag" in iJSON.keys(): 834 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 835 836 if "amortizationFlag" in iJSON.keys(): 837 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 838 839 info.append(splitLine) 840 841 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 842 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 843 844 if iJSON["figi"]: 845 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 846 847 info.extend([ 848 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 849 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 850 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 851 ]) 852 853 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 854 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 855 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 856 iJSON["aciValue"]["currency"] 857 ))) 858 859 if "currentPrice" in iJSON.keys(): 860 info.append(splitLine) 861 862 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 863 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 864 865 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 866 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 867 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 868 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 869 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 870 871 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 872 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 873 874 info.extend([ 875 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 880 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 881 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 882 )), 883 "| Changes between last deal price and last close | {:<54} |\n".format( 884 "{:.2f}%{}".format( 885 iJSON["currentPrice"]["changes"], 886 " ({}{:.2f} {})".format( 887 "+" if bondChangesDelta > 0 else "", 888 bondChangesDelta, 889 aciCurrency 890 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 891 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 892 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 893 currency 894 ), 895 ) 896 ), 897 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 898 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 899 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 900 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 903 )), 904 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 905 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 906 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 907 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 908 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 909 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 910 )), 911 ]) 912 913 if "lot" in iJSON.keys(): 914 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 915 916 if "step" in iJSON.keys() and iJSON["step"] != 0: 917 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 918 919 # Add bond payment calendar: 920 if iJSON["type"] == "Bonds": 921 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 922 info.extend(["\n", strCalendar]) 923 924 infoText += "".join(info) 925 926 if show: 927 uLogger.info("{}".format(infoText)) 928 929 else: 930 uLogger.debug("{}".format(infoText)) 931 932 if self.infoFile is not None: 933 with open(self.infoFile, "w", encoding="UTF-8") as fH: 934 fH.write(infoText) 935 936 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 937 938 return infoText 939 940 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 941 """ 942 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 943 944 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 945 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 946 :return: JSON formatted data with information about instrument. 947 """ 948 tickerJSON = {} 949 if self.moreDebug: 950 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 951 952 if not self.ticker: 953 uLogger.warning("self.ticker variable is not be empty!") 954 955 else: 956 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 957 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 958 raise Exception("Instrument not allowed") 959 960 if not self.iList: 961 self.iList = self.Listing() 962 963 if self.ticker in self.iList["Shares"].keys(): 964 tickerJSON = self.iList["Shares"][self.ticker] 965 if self.moreDebug: 966 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 967 968 elif self.ticker in self.iList["Currencies"].keys(): 969 tickerJSON = self.iList["Currencies"][self.ticker] 970 if self.moreDebug: 971 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 972 973 elif self.ticker in self.iList["Bonds"].keys(): 974 tickerJSON = self.iList["Bonds"][self.ticker] 975 if self.moreDebug: 976 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 977 978 elif self.ticker in self.iList["Etfs"].keys(): 979 tickerJSON = self.iList["Etfs"][self.ticker] 980 if self.moreDebug: 981 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 982 983 elif self.ticker in self.iList["Futures"].keys(): 984 tickerJSON = self.iList["Futures"][self.ticker] 985 if self.moreDebug: 986 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 987 988 if tickerJSON: 989 self.figi = tickerJSON["figi"] 990 991 if requestPrice: 992 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 993 994 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 995 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 996 997 else: 998 tickerJSON["currentPrice"]["changes"] = 0 999 1000 if show: 1001 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1002 1003 else: 1004 if show: 1005 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1006 1007 return tickerJSON 1008 1009 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1010 """ 1011 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if self.moreDebug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if self.moreDebug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if self.moreDebug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if self.moreDebug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if self.moreDebug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if self.moreDebug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON 1102 1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1106 `{"buy": [{"price": 1243.8, "quantity": 193}, 1107 {"price": 1244.0, "quantity": 168}, 1108 {"price": 1244.8, "quantity": 5}, 1109 {"price": 1245.0, "quantity": 61}, 1110 {"price": 1245.4, "quantity": 60}], 1111 "sell": [{"price": 1243.6, "quantity": 8}, 1112 {"price": 1242.6, "quantity": 10}, 1113 {"price": 1242.4, "quantity": 18}, 1114 {"price": 1242.2, "quantity": 50}, 1115 {"price": 1242.0, "quantity": 113}], 1116 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1117 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1118 - sell: list of dicts with Buyers prices, 1119 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1120 - quantity: volume value by current price in lots, 1121 - limitUp: current trade session limit price, maximum, 1122 - limitDown: current trade session limit price, minimum, 1123 - lastPrice: last deal price of the instrument, 1124 - closePrice: previous trade session close price of the instrument. 1125 1126 See also: `SearchByTicker()` and `SearchByFIGI()`. 1127 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1128 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1129 1130 :param show: if `True` then print DOM to log and console. 1131 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1132 If an error occurred then returns an empty record: 1133 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1134 """ 1135 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1136 1137 if self.depth < 1: 1138 uLogger.error("Depth of Market (DOM) must be >=1!") 1139 raise Exception("Incorrect value") 1140 1141 if not (self.ticker or self.figi): 1142 uLogger.error("self.ticker or self.figi variables must be defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 if self.ticker and not self.figi: 1146 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1147 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1148 1149 if not self.ticker and self.figi: 1150 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1152 1153 if not self.figi: 1154 uLogger.error("FIGI is not defined!") 1155 raise Exception("Ticker or FIGI required") 1156 1157 else: 1158 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1159 1160 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1161 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1162 self.body = str({"figi": self.figi, "depth": self.depth}) 1163 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1164 1165 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1166 # list of dicts with sellers orders: 1167 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1168 1169 # list of dicts with buyers orders: 1170 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1171 1172 # max price of instrument at this time: 1173 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1174 1175 # min price of instrument at this time: 1176 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1177 1178 # last price of deal with instrument: 1179 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1180 1181 # last close price of instrument: 1182 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1183 1184 else: 1185 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1186 uLogger.debug("Server response: {}".format(pricesResponse)) 1187 1188 if show: 1189 if prices["buy"] or prices["sell"]: 1190 info = [ 1191 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1192 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1193 self.ticker, 1194 self.figi, 1195 self.depth, 1196 ), 1197 "-" * 60, "\n", 1198 " Orders of Buyers | Orders of Sellers\n", 1199 "-" * 60, "\n", 1200 " Sell prices (volumes) | Buy prices (volumes)\n", 1201 "-" * 60, "\n", 1202 ] 1203 1204 if not prices["buy"]: 1205 info.append(" | No orders!\n") 1206 sumBuy = 0 1207 1208 else: 1209 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1210 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1211 for item in maxMinSorted: 1212 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1213 1214 if not prices["sell"]: 1215 info.append("No orders! |\n") 1216 sumSell = 0 1217 1218 else: 1219 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1220 for item in prices["sell"]: 1221 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1222 1223 info.extend([ 1224 "-" * 60, "\n", 1225 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1226 "-" * 60, "\n", 1227 ]) 1228 1229 infoText = "".join(info) 1230 1231 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1232 1233 else: 1234 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1235 1236 return prices 1237 1238 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1239 """ 1240 This method get and show information about all available broker instruments for current user account. 1241 If `instrumentsFile` string is not empty then also save information to this file. 1242 1243 :param show: if `True` then print results to console, if `False` — print only to file. 1244 :return: multi-lines string with all available broker instruments 1245 """ 1246 if not self.iList: 1247 self.iList = self.Listing() 1248 1249 info = [ 1250 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1251 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1252 ] 1253 1254 # add instruments count by type: 1255 for iType in self.iList.keys(): 1256 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1257 1258 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1259 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1260 1261 # generating info tables with all instruments by type: 1262 for iType in self.iList.keys(): 1263 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1264 1265 for instrument in self.iList[iType].keys(): 1266 iName = self.iList[iType][instrument]["name"] # instrument's name 1267 if len(iName) > 57: 1268 iName = "{}...".format(iName[:54]) # right trim for a long string 1269 1270 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1271 self.iList[iType][instrument]["ticker"], 1272 iName, 1273 self.iList[iType][instrument]["figi"], 1274 self.iList[iType][instrument]["currency"], 1275 self.iList[iType][instrument]["lot"], 1276 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1277 )) 1278 1279 infoText = "".join(info) 1280 1281 if show: 1282 uLogger.info(infoText) 1283 1284 if self.instrumentsFile: 1285 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1286 fH.write(infoText) 1287 1288 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1289 1290 return infoText 1291 1292 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` — return list of result only. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile: 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 return searchResults 1370 1371 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1372 """ 1373 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1374 1375 :param instruments: list of strings with tickers or FIGIs. 1376 :return: list with unique instrument FIGIs only. 1377 """ 1378 requestedInstruments = [] 1379 for iName in instruments: 1380 if iName not in self.aliases.keys(): 1381 if iName not in requestedInstruments: 1382 requestedInstruments.append(iName) 1383 1384 else: 1385 if iName not in requestedInstruments: 1386 if self.aliases[iName] not in requestedInstruments: 1387 requestedInstruments.append(self.aliases[iName]) 1388 1389 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1390 1391 onlyUniqueFIGIs = [] 1392 for iName in requestedInstruments: 1393 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1394 continue 1395 1396 self.ticker = iName 1397 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1398 1399 if not iData: 1400 self.ticker = "" 1401 self.figi = iName 1402 1403 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1404 1405 if not iData: 1406 self.figi = "" 1407 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1408 1409 if iData and iData["figi"] not in onlyUniqueFIGIs: 1410 onlyUniqueFIGIs.append(iData["figi"]) 1411 1412 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1413 1414 return onlyUniqueFIGIs 1415 1416 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1417 """ 1418 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1419 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 1422 If `pricesFile` string is not empty then also save information to this file. 1423 1424 :param instruments: list of strings with tickers or FIGIs. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1427 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1428 """ 1429 if instruments is None or not instruments: 1430 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1431 raise Exception("Ticker or FIGI required") 1432 1433 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1434 1435 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1436 1437 iList = [] # trying to get info and current prices about all unique instruments: 1438 for self.figi in onlyUniqueFIGIs: 1439 iData = self.SearchByFIGI(requestPrice=True) 1440 iList.append(iData) 1441 1442 self.ShowListOfPrices(iList, show) 1443 1444 return iList 1445 1446 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1447 """ 1448 Show table contains current prices of given instruments. 1449 1450 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1451 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1452 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1453 :return: multilines text in Markdown format as a table contains current prices. 1454 """ 1455 infoText = "" 1456 1457 if show or self.pricesFile: 1458 info = [ 1459 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1460 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1461 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1462 ] 1463 1464 for item in iList: 1465 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1466 item["ticker"], 1467 item["figi"], 1468 item["type"], 1469 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1470 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1471 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1472 "{} / {}".format( 1473 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1474 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1475 ), 1476 "{} / {}".format( 1477 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1478 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1479 ), 1480 item["currency"], 1481 )) 1482 1483 infoText = "".join(info) 1484 1485 if show: 1486 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1487 1488 if self.pricesFile: 1489 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1490 fH.write(infoText) 1491 1492 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1493 1494 return infoText 1495 1496 def RequestTradingStatus(self) -> dict: 1497 """ 1498 Requesting trading status for the instrument defined by `figi` variable. 1499 1500 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1501 1502 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1503 1504 :return: dictionary with trading status attributes. Response example: 1505 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1506 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1507 """ 1508 if self.figi is None or not self.figi: 1509 uLogger.error("Variable `figi` must be defined for using this method!") 1510 raise Exception("FIGI required") 1511 1512 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1513 1514 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1515 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1516 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1517 1518 if self.moreDebug: 1519 uLogger.debug("Records about current trading status successfully received") 1520 1521 return tradingStatus 1522 1523 def RequestPortfolio(self) -> dict: 1524 """ 1525 Requesting actual user's portfolio for current `accountId`. 1526 1527 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1528 1529 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1530 1531 :return: dictionary with user's portfolio. 1532 """ 1533 if self.accountId is None or not self.accountId: 1534 uLogger.error("Variable `accountId` must be defined for using this method!") 1535 raise Exception("Account ID required") 1536 1537 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1538 1539 self.body = str({"accountId": self.accountId}) 1540 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1541 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1542 1543 if self.moreDebug: 1544 uLogger.debug("Records about user's portfolio successfully received") 1545 1546 return rawPortfolio 1547 1548 def RequestPositions(self) -> dict: 1549 """ 1550 Requesting open positions by currencies and instruments for current `accountId`. 1551 1552 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1553 1554 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1555 1556 :return: dictionary with open positions by instruments. 1557 """ 1558 if self.accountId is None or not self.accountId: 1559 uLogger.error("Variable `accountId` must be defined for using this method!") 1560 raise Exception("Account ID required") 1561 1562 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1563 1564 self.body = str({"accountId": self.accountId}) 1565 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1566 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1567 1568 if self.moreDebug: 1569 uLogger.debug("Records about current open positions successfully received") 1570 1571 return rawPositions 1572 1573 def RequestPendingOrders(self) -> list: 1574 """ 1575 Requesting current actual pending orders for current `accountId`. 1576 1577 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1578 1579 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1580 1581 :return: list of dictionaries with pending orders. 1582 """ 1583 if self.accountId is None or not self.accountId: 1584 uLogger.error("Variable `accountId` must be defined for using this method!") 1585 raise Exception("Account ID required") 1586 1587 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1588 1589 self.body = str({"accountId": self.accountId}) 1590 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1591 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1592 1593 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1594 1595 return rawOrders 1596 1597 def RequestStopOrders(self) -> list: 1598 """ 1599 Requesting current actual stop orders for current `accountId`. 1600 1601 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1602 1603 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1604 1605 :return: list of dictionaries with stop orders. 1606 """ 1607 if self.accountId is None or not self.accountId: 1608 uLogger.error("Variable `accountId` must be defined for using this method!") 1609 raise Exception("Account ID required") 1610 1611 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1612 1613 self.body = str({"accountId": self.accountId}) 1614 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1615 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1616 1617 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1618 1619 return rawStopOrders 1620 1621 def Overview(self, show: bool = False, details: str = "full") -> dict: 1622 """ 1623 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1624 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1625 and `overviewBondsCalendarFile` are defined then also save information to file. 1626 1627 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1628 many requests about the state of the portfolio, and then, based on the received data, a large number 1629 of calculation and statistics are collected. 1630 1631 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1632 :param details: how detailed should the information be? 1633 - `full` — shows full available information about portfolio status (by default), 1634 - `positions` — shows only open positions, 1635 - `orders` — shows only sections of open limits and stop orders. 1636 - `digest` — show a short digest of the portfolio status, 1637 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1638 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1639 :return: dictionary with client's raw portfolio and some statistics. 1640 """ 1641 if self.accountId is None or not self.accountId: 1642 uLogger.error("Variable `accountId` must be defined for using this method!") 1643 raise Exception("Account ID required") 1644 1645 view = { 1646 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1647 "headers": {}, # list of dictionaries, response headers without "positions" section 1648 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1649 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1650 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1651 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1652 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1653 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1654 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1655 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1656 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1657 }, 1658 "stat": { # --- some statistics calculated using "raw" sections: 1659 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1660 "availableRUB": 0., # available rubles (without other currencies) 1661 "blockedRUB": 0., # blocked sum in Russian Rouble 1662 "totalChangesRUB": 0., # changes for all open trades in RUB 1663 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1664 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1665 "sharesCostRUB": 0., # costs of all shares in RUB 1666 "bondsCostRUB": 0., # costs of all bonds in RUB 1667 "etfsCostRUB": 0., # costs of all etfs in RUB 1668 "futuresCostRUB": 0., # costs of all futures in RUB 1669 "Currencies": [], # list of dictionaries of all currencies statistics 1670 "Shares": [], # list of dictionaries of all shares statistics 1671 "Bonds": [], # list of dictionaries of all bonds statistics 1672 "Etfs": [], # list of dictionaries of all etfs statistics 1673 "Futures": [], # list of dictionaries of all futures statistics 1674 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1675 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1676 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1677 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1678 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1679 }, 1680 "analytics": { # --- some analytics of portfolio: 1681 "distrByAssets": {}, # portfolio distribution by assets 1682 "distrByCompanies": {}, # portfolio distribution by companies 1683 "distrBySectors": {}, # portfolio distribution by sectors 1684 "distrByCurrencies": {}, # portfolio distribution by currencies 1685 "distrByCountries": {}, # portfolio distribution by countries 1686 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1687 } 1688 } 1689 1690 details = details.lower() 1691 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1692 if details not in availableDetails: 1693 details = "full" 1694 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1695 1696 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1697 1698 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1699 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1700 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1701 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1702 1703 # save response headers without "positions" section: 1704 for key in portfolioResponse.keys(): 1705 if key != "positions": 1706 view["raw"]["headers"][key] = portfolioResponse[key] 1707 1708 else: 1709 continue 1710 1711 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1712 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1713 for item in portfolioResponse["positions"]: 1714 if item["instrumentType"] == "currency": 1715 self.figi = item["figi"] 1716 curr = self.SearchByFIGI(requestPrice=False) 1717 1718 # current price of currency in RUB: 1719 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1720 "name": curr["name"], 1721 "currentPrice": NanoToFloat( 1722 item["currentPrice"]["units"], 1723 item["currentPrice"]["nano"] 1724 ), 1725 } 1726 1727 view["raw"]["Currencies"].append(item) 1728 1729 elif item["instrumentType"] == "share": 1730 view["raw"]["Shares"].append(item) 1731 1732 elif item["instrumentType"] == "bond": 1733 view["raw"]["Bonds"].append(item) 1734 1735 elif item["instrumentType"] == "etf": 1736 view["raw"]["Etfs"].append(item) 1737 1738 elif item["instrumentType"] == "futures": 1739 view["raw"]["Futures"].append(item) 1740 1741 else: 1742 continue 1743 1744 # how many volume of currencies (by ISO currency name) are blocked: 1745 for item in view["raw"]["positions"]["blocked"]: 1746 blocked = NanoToFloat(item["units"], item["nano"]) 1747 if blocked > 0: 1748 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1749 1750 # how many volume of instruments (by FIGI) are blocked: 1751 for item in view["raw"]["positions"]["securities"]: 1752 blocked = int(item["blocked"]) 1753 if blocked > 0: 1754 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1755 1756 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1757 1758 if "rub" in allBlocked.keys(): 1759 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1760 1761 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1762 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1763 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1764 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1765 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1766 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1767 view["stat"]["portfolioCostRUB"] = sum([ 1768 view["stat"]["allCurrenciesCostRUB"], 1769 view["stat"]["sharesCostRUB"], 1770 view["stat"]["bondsCostRUB"], 1771 view["stat"]["etfsCostRUB"], 1772 view["stat"]["futuresCostRUB"], 1773 ]) 1774 1775 # --- calculating some portfolio statistics: 1776 byComp = {} # distribution by companies 1777 bySect = {} # distribution by sectors 1778 byCurr = {} # distribution by currencies (include RUB) 1779 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1780 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1781 1782 for item in portfolioResponse["positions"]: 1783 self.figi = item["figi"] 1784 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1785 1786 if instrument: 1787 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1788 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1789 1790 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1791 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1792 1793 else: 1794 blocked = 0 1795 1796 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1797 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1798 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1799 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1800 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1801 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1802 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1803 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1804 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1805 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1806 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1807 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1808 1809 statData = { 1810 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1811 "ticker": instrument["ticker"], # ticker by FIGI 1812 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1813 "volume": volume, # available volume of instrument 1814 "lots": lots, # volume in lots of instrument 1815 "direction": direction, # direction of an instrument's position: short or long 1816 "blocked": blocked, # blocked volume of currency or instrument 1817 "currentPrice": curPrice, # current instrument's price in basic asset 1818 "average": average, # current average position price 1819 "cost": cost, # current cost of all volume of instrument in basic asset 1820 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1821 "costRUB": costRUB, # cost of instrument in ruble 1822 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1823 "profit": profit, # expected profit at current moment 1824 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1825 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1826 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1827 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1828 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1829 "step": instrument["step"], # minimum price increment 1830 } 1831 1832 # adding distribution by unique countries: 1833 if statData["country"] not in byCountry.keys(): 1834 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1835 1836 else: 1837 byCountry[statData["country"]]["cost"] += costRUB 1838 byCountry[statData["country"]]["percent"] += percentCostRUB 1839 1840 if item["instrumentType"] != "currency": 1841 # adding distribution by unique companies: 1842 if statData["name"]: 1843 if statData["name"] not in byComp.keys(): 1844 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 byComp[statData["name"]]["cost"] += costRUB 1848 byComp[statData["name"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique sectors: 1851 if statData["sector"] not in bySect.keys(): 1852 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1853 1854 else: 1855 bySect[statData["sector"]]["cost"] += costRUB 1856 bySect[statData["sector"]]["percent"] += percentCostRUB 1857 1858 # adding distribution by unique currencies: 1859 if currency not in byCurr.keys(): 1860 byCurr[currency] = { 1861 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1862 "cost": costRUB, 1863 "percent": percentCostRUB 1864 } 1865 1866 else: 1867 byCurr[currency]["cost"] += costRUB 1868 byCurr[currency]["percent"] += percentCostRUB 1869 1870 # saving statistics for every instrument: 1871 if item["instrumentType"] == "currency": 1872 view["stat"]["Currencies"].append(statData) 1873 1874 # update dict with free funds for trading (total - blocked) by currencies 1875 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1876 view["stat"]["funds"][currency] = { 1877 "total": volume, 1878 "totalCostRUB": costRUB, # total volume cost in rubles 1879 "free": volume - blocked, 1880 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1881 } 1882 1883 elif item["instrumentType"] == "share": 1884 view["stat"]["Shares"].append(statData) 1885 1886 elif item["instrumentType"] == "bond": 1887 view["stat"]["Bonds"].append(statData) 1888 1889 elif item["instrumentType"] == "etf": 1890 view["stat"]["Etfs"].append(statData) 1891 1892 elif item["instrumentType"] == "Futures": 1893 view["stat"]["Futures"].append(statData) 1894 1895 else: 1896 continue 1897 1898 # total changes in Russian Ruble: 1899 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1900 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1901 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1902 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1903 view["stat"]["funds"]["rub"] = { 1904 "total": view["stat"]["availableRUB"], 1905 "totalCostRUB": view["stat"]["availableRUB"], 1906 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1908 } 1909 1910 # --- pending orders sector data: 1911 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1912 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1913 1914 for item in view["raw"]["orders"]: 1915 self.figi = item["figi"] 1916 1917 if item["figi"] not in uniquePendingOrdersFIGIs: 1918 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1919 1920 uniquePendingOrdersFIGIs.append(item["figi"]) 1921 uniquePendingOrders[item["figi"]] = instrument 1922 1923 else: 1924 instrument = uniquePendingOrders[item["figi"]] 1925 1926 if instrument: 1927 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1928 orderType = TKS_ORDER_TYPES[item["orderType"]] 1929 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1930 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1931 1932 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1933 if item["direction"] == "ORDER_DIRECTION_BUY": 1934 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1935 1936 else: 1937 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1938 1939 # requested price for order execution: 1940 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1941 1942 # necessary changes in percent to reach target from current price: 1943 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1944 1945 view["stat"]["orders"].append({ 1946 "orderID": item["orderId"], # orderId number parameter of current order 1947 "figi": item["figi"], # FIGI identification 1948 "ticker": instrument["ticker"], # ticker name by FIGI 1949 "lotsRequested": item["lotsRequested"], # requested lots value 1950 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for order execution in base currency 1953 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1954 "percentChanges": changes, # changes in percent to target from current price 1955 "currency": item["currency"], # instrument's currency name 1956 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1957 "type": orderType, # type of order from TKS_ORDER_TYPES 1958 "status": orderState, # order status from TKS_ORDER_STATES 1959 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1960 }) 1961 1962 # --- stop orders sector data: 1963 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1964 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["stopOrders"]: 1967 self.figi = item["figi"] 1968 1969 if item["figi"] not in uniqueStopOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniqueStopOrdersFIGIs.append(item["figi"]) 1973 uniqueStopOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniqueStopOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1981 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1982 1983 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1984 if "expirationTime" in item.keys(): 1985 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1986 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1987 1988 else: 1989 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1990 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1991 1992 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1993 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1994 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1995 1996 else: 1997 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1998 1999 # requested price when stop-order executed: 2000 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2001 2002 # price for limit-order, set up when stop-order executed: 2003 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2004 2005 # necessary changes in percent to reach target from current price: 2006 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2007 2008 view["stat"]["stopOrders"].append({ 2009 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2010 "figi": item["figi"], # FIGI identification 2011 "ticker": instrument["ticker"], # ticker name by FIGI 2012 "lotsRequested": item["lotsRequested"], # requested lots value 2013 "currentPrice": lastPrice, # current instrument's price for defined action 2014 "targetPrice": target, # requested price for stop-order execution in base currency 2015 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2016 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2017 "percentChanges": changes, # changes in percent to target from current price 2018 "currency": item["currency"], # instrument's currency name 2019 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2020 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2021 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2022 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2023 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2024 }) 2025 2026 # --- calculating data for analytics section: 2027 # portfolio distribution by assets: 2028 view["analytics"]["distrByAssets"] = { 2029 "Ruble": { 2030 "uniques": 1, 2031 "cost": view["stat"]["availableRUB"], 2032 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Currencies": { 2035 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2036 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2037 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Shares": { 2040 "uniques": len(view["stat"]["Shares"]), 2041 "cost": view["stat"]["sharesCostRUB"], 2042 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Bonds": { 2045 "uniques": len(view["stat"]["Bonds"]), 2046 "cost": view["stat"]["bondsCostRUB"], 2047 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Etfs": { 2050 "uniques": len(view["stat"]["Etfs"]), 2051 "cost": view["stat"]["etfsCostRUB"], 2052 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Futures": { 2055 "uniques": len(view["stat"]["Futures"]), 2056 "cost": view["stat"]["futuresCostRUB"], 2057 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 } 2060 2061 # portfolio distribution by companies: 2062 view["analytics"]["distrByCompanies"]["All money cash"] = { 2063 "ticker": "", 2064 "cost": view["stat"]["allCurrenciesCostRUB"], 2065 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2066 } 2067 view["analytics"]["distrByCompanies"].update(byComp) 2068 2069 # portfolio distribution by sectors: 2070 view["analytics"]["distrBySectors"]["All money cash"] = { 2071 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2072 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2073 } 2074 view["analytics"]["distrBySectors"].update(bySect) 2075 2076 # portfolio distribution by currencies: 2077 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2078 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2079 2080 if self.moreDebug: 2081 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2082 2083 view["analytics"]["distrByCurrencies"].update(byCurr) 2084 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2085 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2086 2087 # portfolio distribution by countries: 2088 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2089 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2090 2091 if self.moreDebug: 2092 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2093 2094 view["analytics"]["distrByCountries"].update(byCountry) 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2096 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2097 2098 # --- Prepare text statistics overview in human-readable: 2099 if show: 2100 # Whatever the value `details`, header not changes: 2101 info = [ 2102 "# Client's portfolio\n\n", 2103 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2104 "* **Account ID:** [{}]\n".format(self.accountId), 2105 ] 2106 2107 if details in ["full", "positions", "digest"]: 2108 info.extend([ 2109 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2110 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2111 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2112 view["stat"]["totalChangesRUB"], 2113 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2114 view["stat"]["totalChangesPercentRUB"], 2115 ), 2116 ]) 2117 2118 if details in ["full", "positions"]: 2119 info.extend([ 2120 "## Open positions\n\n", 2121 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2122 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2123 "| Ruble | {:>31} | | | | | |\n".format( 2124 "{:.2f} ({:.2f}) rub".format( 2125 view["stat"]["availableRUB"], 2126 view["stat"]["blockedRUB"], 2127 ) 2128 ) 2129 ]) 2130 2131 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2132 return [ 2133 "| | | | | | | |\n", 2134 "| {:<27} | | | | | {:>19} | |\n".format( 2135 noTradeStr if noTradeStr else typeStr, 2136 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2137 ), 2138 ] 2139 2140 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2141 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2142 "{} [{}]".format(data["ticker"], data["figi"]), 2143 "{:.2f} ({:.2f}) {}".format( 2144 data["volume"], 2145 data["blocked"], 2146 data["currency"], 2147 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2148 data["volume"], 2149 data["blocked"], 2150 ), 2151 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2152 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2153 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2154 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2155 "{}{:.2f} {} ({}{:.2f}%)".format( 2156 "+" if data["profit"] > 0 else "", 2157 data["profit"], data["baseCurrencyName"], 2158 "+" if data["percentProfit"] > 0 else "", 2159 data["percentProfit"], 2160 ), 2161 ) 2162 2163 # --- Show currencies section: 2164 if view["stat"]["Currencies"]: 2165 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2166 for item in view["stat"]["Currencies"]: 2167 info.append(_InfoStr(item, showCurrencyName=True)) 2168 2169 else: 2170 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2171 2172 # --- Show shares section: 2173 if view["stat"]["Shares"]: 2174 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2175 2176 for item in view["stat"]["Shares"]: 2177 info.append(_InfoStr(item)) 2178 2179 else: 2180 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2181 2182 # --- Show bonds section: 2183 if view["stat"]["Bonds"]: 2184 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2185 2186 for item in view["stat"]["Bonds"]: 2187 info.append(_InfoStr(item)) 2188 2189 else: 2190 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2191 2192 # --- Show etfs section: 2193 if view["stat"]["Etfs"]: 2194 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2195 2196 for item in view["stat"]["Etfs"]: 2197 info.append(_InfoStr(item)) 2198 2199 else: 2200 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2201 2202 # --- Show futures section: 2203 if view["stat"]["Futures"]: 2204 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2205 2206 for item in view["stat"]["Futures"]: 2207 info.append(_InfoStr(item)) 2208 2209 else: 2210 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2211 2212 if details in ["full", "orders"]: 2213 # --- Show pending orders section: 2214 if view["stat"]["orders"]: 2215 info.extend([ 2216 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2217 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2218 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["orders"]: 2222 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 item["action"], 2234 item["type"], 2235 item["date"], 2236 )) 2237 2238 else: 2239 info.append("\n## Total pending limit-orders: 0\n") 2240 2241 # --- Show stop orders section: 2242 if view["stat"]["stopOrders"]: 2243 info.extend([ 2244 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2245 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2246 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2247 ]) 2248 2249 for item in view["stat"]["stopOrders"]: 2250 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2251 "{} [{}]".format(item["ticker"], item["figi"]), 2252 item["orderID"], 2253 item["lotsRequested"], 2254 "{} {} ({}{:.2f}%)".format( 2255 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2256 item["baseCurrencyName"], 2257 "+" if item["percentChanges"] > 0 else "", 2258 float(item["percentChanges"]), 2259 ), 2260 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2261 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2262 item["action"], 2263 item["type"], 2264 item["expType"], 2265 item["createDate"], 2266 item["expDate"], 2267 )) 2268 2269 else: 2270 info.append("\n## Total stop-orders: 0\n") 2271 2272 if details in ["full", "analytics"]: 2273 # -- Show analytics section: 2274 if view["stat"]["portfolioCostRUB"] > 0: 2275 info.extend([ 2276 "\n# Analytics\n" 2277 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2278 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2279 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2280 view["stat"]["totalChangesRUB"], 2281 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2282 view["stat"]["totalChangesPercentRUB"], 2283 ), 2284 "\n## Portfolio distribution by assets\n" 2285 "\n| Type | Uniques | Percent | Current cost |\n", 2286 "|------------------------------------|---------|---------|--------------------|\n", 2287 ]) 2288 2289 for key in view["analytics"]["distrByAssets"].keys(): 2290 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2291 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2292 key, 2293 view["analytics"]["distrByAssets"][key]["uniques"], 2294 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2295 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2296 )) 2297 2298 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2299 2300 info.extend([ 2301 "\n## Portfolio distribution by companies\n" 2302 "\n| Company | Percent | Current cost |\n", 2303 aSepLine, 2304 ]) 2305 2306 for company in view["analytics"]["distrByCompanies"].keys(): 2307 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2308 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2309 "{}{}".format( 2310 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2311 company, 2312 ), 2313 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2314 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2315 )) 2316 2317 info.extend([ 2318 "\n## Portfolio distribution by sectors\n" 2319 "\n| Sector | Percent | Current cost |\n", 2320 aSepLine, 2321 ]) 2322 2323 for sector in view["analytics"]["distrBySectors"].keys(): 2324 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2325 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2326 sector, 2327 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2328 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2329 )) 2330 2331 info.extend([ 2332 "\n## Portfolio distribution by currencies\n" 2333 "\n| Instruments currencies | Percent | Current cost |\n", 2334 aSepLine, 2335 ]) 2336 2337 for curr in view["analytics"]["distrByCurrencies"].keys(): 2338 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2339 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2340 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2341 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2343 )) 2344 2345 info.extend([ 2346 "\n## Portfolio distribution by countries\n" 2347 "\n| Assets by country | Percent | Current cost |\n", 2348 aSepLine, 2349 ]) 2350 2351 for country in view["analytics"]["distrByCountries"].keys(): 2352 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2353 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2354 country, 2355 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2356 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2357 )) 2358 2359 if details in ["full", "calendar"]: 2360 # -- Show bonds payment calendar section: 2361 if view["stat"]["Bonds"]: 2362 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2363 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2364 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2365 2366 else: 2367 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2368 2369 infoText = "".join(info) 2370 2371 uLogger.info(infoText) 2372 2373 if details == "full" and self.overviewFile: 2374 filename = self.overviewFile 2375 2376 elif details == "digest" and self.overviewDigestFile: 2377 filename = self.overviewDigestFile 2378 2379 elif details == "positions" and self.overviewPositionsFile: 2380 filename = self.overviewPositionsFile 2381 2382 elif details == "orders" and self.overviewOrdersFile: 2383 filename = self.overviewOrdersFile 2384 2385 elif details == "analytics" and self.overviewAnalyticsFile: 2386 filename = self.overviewAnalyticsFile 2387 2388 elif details == "calendar" and self.overviewBondsCalendarFile: 2389 filename = self.overviewBondsCalendarFile 2390 2391 else: 2392 filename = "" 2393 2394 if filename: 2395 with open(filename, "w", encoding="UTF-8") as fH: 2396 fH.write(infoText) 2397 2398 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2399 2400 return view 2401 2402 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2403 """ 2404 Returns history operations between two given dates for current `accountId`. 2405 If `reportFile` string is not empty then also save human-readable report. 2406 Shows some statistical data of closed positions. 2407 2408 :param start: see docstring in `GetDatesAsString()` method 2409 :param end: see docstring in `GetDatesAsString()` method 2410 :param show: if `True` then also prints all records to the console. 2411 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2412 :return: original list of dictionaries with history of deals records from API ("operations" key): 2413 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2414 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2415 """ 2416 if self.accountId is None or not self.accountId: 2417 uLogger.error("Variable `accountId` must be defined for using this method!") 2418 raise Exception("Account ID required") 2419 2420 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2421 2422 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2423 2424 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2425 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2426 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2427 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2428 customStat = {} # custom statistics in additional to responseJSON 2429 2430 # --- output report in human-readable format: 2431 if show or self.reportFile: 2432 splitLine1 = "| | | | | |\n" # Summary section 2433 splitLine2 = "| | | | | | | | |\n" # Operations section 2434 nextDay = "" 2435 2436 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2437 2438 if len(ops) > 0: 2439 customStat = { 2440 "opsCount": 0, # total operations count 2441 "buyCount": 0, # buy operations 2442 "sellCount": 0, # sell operations 2443 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2444 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2445 "payIn": {"rub": 0.}, # Deposit brokerage account 2446 "payOut": {"rub": 0.}, # Withdrawals 2447 "divs": {"rub": 0.}, # Dividends income 2448 "coupons": {"rub": 0.}, # Coupon's income 2449 "brokerCom": {"rub": 0.}, # Service commissions 2450 "serviceCom": {"rub": 0.}, # Service commissions 2451 "marginCom": {"rub": 0.}, # Margin commissions 2452 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2453 } 2454 2455 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2456 for item in ops: 2457 if item["state"] == "OPERATION_STATE_EXECUTED": 2458 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2459 2460 # count buy operations: 2461 if "_BUY" in item["operationType"]: 2462 customStat["buyCount"] += 1 2463 2464 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2465 customStat["buyTotal"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["buyTotal"][item["payment"]["currency"]] = payment 2469 2470 # count sell operations: 2471 elif "_SELL" in item["operationType"]: 2472 customStat["sellCount"] += 1 2473 2474 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2475 customStat["sellTotal"][item["payment"]["currency"]] += payment 2476 2477 else: 2478 customStat["sellTotal"][item["payment"]["currency"]] = payment 2479 2480 # count incoming operations: 2481 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2482 if item["payment"]["currency"] in customStat["payIn"].keys(): 2483 customStat["payIn"][item["payment"]["currency"]] += payment 2484 2485 else: 2486 customStat["payIn"][item["payment"]["currency"]] = payment 2487 2488 # count withdrawals operations: 2489 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2490 if item["payment"]["currency"] in customStat["payOut"].keys(): 2491 customStat["payOut"][item["payment"]["currency"]] += payment 2492 2493 else: 2494 customStat["payOut"][item["payment"]["currency"]] = payment 2495 2496 # count dividends income: 2497 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2498 if item["payment"]["currency"] in customStat["divs"].keys(): 2499 customStat["divs"][item["payment"]["currency"]] += payment 2500 2501 else: 2502 customStat["divs"][item["payment"]["currency"]] = payment 2503 2504 # count coupon's income: 2505 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2506 if item["payment"]["currency"] in customStat["coupons"].keys(): 2507 customStat["coupons"][item["payment"]["currency"]] += payment 2508 2509 else: 2510 customStat["coupons"][item["payment"]["currency"]] = payment 2511 2512 # count broker commissions: 2513 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2514 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2515 customStat["brokerCom"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["brokerCom"][item["payment"]["currency"]] = payment 2519 2520 # count service commissions: 2521 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2522 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2523 customStat["serviceCom"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["serviceCom"][item["payment"]["currency"]] = payment 2527 2528 # count margin commissions: 2529 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2530 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2531 customStat["marginCom"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["marginCom"][item["payment"]["currency"]] = payment 2535 2536 # count withholding taxes: 2537 elif "_TAX" in item["operationType"]: 2538 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2539 customStat["allTaxes"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["allTaxes"][item["payment"]["currency"]] = payment 2543 2544 else: 2545 continue 2546 2547 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2548 2549 # --- view "Actions" lines: 2550 info.extend([ 2551 "| Report sections | | | | |\n", 2552 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2553 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2554 "| | Buy: {:<22} | {:<28} | | |\n".format( 2555 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2556 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2557 ), 2558 "| | Sell: {:<21} | {:<28} | | |\n".format( 2559 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2560 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2561 ), 2562 ]) 2563 2564 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2565 for key in opsKeys: 2566 if key == "rub": 2567 continue 2568 2569 info.extend([ 2570 "| | | {:<28} | | |\n".format( 2571 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2572 ), 2573 "| | | {:<28} | | |\n".format( 2574 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2575 ), 2576 ]) 2577 2578 info.append(splitLine1) 2579 2580 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2581 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2582 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2583 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2584 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2586 ) 2587 2588 # --- view "Payments" lines: 2589 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2590 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2591 2592 for key in paymentsKeys: 2593 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2594 2595 info.append(splitLine1) 2596 2597 # --- view "Commissions and taxes" lines: 2598 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2599 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2600 2601 for key in comKeys: 2602 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2603 2604 info.append(splitLine1) 2605 2606 info.extend([ 2607 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2608 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2609 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2610 ]) 2611 2612 else: 2613 info.append("Broker returned no operations during this period\n") 2614 2615 # --- view "Operations" section: 2616 for item in ops: 2617 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2618 continue 2619 2620 else: 2621 self.figi = item["figi"] if item["figi"] else "" 2622 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2623 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2624 2625 # group of deals during one day: 2626 if nextDay and item["date"].split("T")[0] != nextDay: 2627 info.append(splitLine2) 2628 nextDay = "" 2629 2630 else: 2631 nextDay = item["date"].split("T")[0] # saving current day for splitting 2632 2633 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2634 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2635 self.figi if self.figi else "—", 2636 instrument["ticker"] if instrument else "—", 2637 instrument["type"] if instrument else "—", 2638 item["quantity"] if int(item["quantity"]) > 0 else "—", 2639 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2640 TKS_OPERATION_STATES[item["state"]], 2641 TKS_OPERATION_TYPES[item["operationType"]], 2642 )) 2643 2644 infoText = "".join(info) 2645 2646 if show: 2647 if self.moreDebug: 2648 uLogger.debug("Records about history of a client's operations successfully received") 2649 2650 uLogger.info(infoText) 2651 2652 if self.reportFile: 2653 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2654 fH.write(infoText) 2655 2656 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2657 2658 return ops, customStat 2659 2660 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2661 """ 2662 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2663 2664 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2665 Warning! Broker server used ISO UTC time by default. 2666 2667 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2668 Also, `historyFile` used to update history with `onlyMissing` parameter. 2669 2670 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2671 2672 :param start: see docstring in `GetDatesAsString()` method. 2673 :param end: see docstring in `GetDatesAsString()` method. 2674 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2675 `"hour"`, `"day"`. Default: `"hour"`. 2676 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2677 False by default. Warning! History appends only from last candle to current time 2678 with always update last candle! 2679 :param csvSep: separator if csv-file is used, `,` by default. 2680 :param show: if `True` then also prints Pandas DataFrame to the console. 2681 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2682 `["date", "time", "open", "high", "low", "close", "volume"]`. 2683 """ 2684 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2685 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2686 history = None # empty pandas object for history 2687 2688 if interval not in TKS_CANDLE_INTERVALS.keys(): 2689 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2690 raise Exception("Incorrect value") 2691 2692 if not (self.ticker or self.figi): 2693 uLogger.error("Ticker or FIGI must be defined!") 2694 raise Exception("Ticker or FIGI required") 2695 2696 if self.ticker and not self.figi: 2697 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2698 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2699 2700 if self.figi and not self.ticker: 2701 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2702 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2703 2704 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2705 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2706 if interval.lower() != "day": 2707 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2708 2709 delta = dtEnd - dtStart # current UTC time minus last time in file 2710 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2711 2712 # calculate history length in candles: 2713 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2714 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2715 length += 1 # to avoid fraction time 2716 2717 # calculate data blocks count: 2718 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2719 2720 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2721 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2722 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2723 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2724 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2725 2726 tempOld = None # pandas object for old history, if --only-missing key present 2727 lastTime = None # datetime object of last old candle in file 2728 2729 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2730 uLogger.debug("--only-missing key present, add only last missing candles...") 2731 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2732 2733 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2734 2735 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2736 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2737 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2738 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2739 2740 # get last datetime object from last string in file or minus 1 delta if file is empty: 2741 if len(tempOld) > 0: 2742 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2743 2744 else: 2745 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2746 2747 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2748 2749 responseJSONs = [] # raw history blocks of data 2750 2751 blockEnd = dtEnd 2752 for item in range(blocks): 2753 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2754 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2755 2756 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2757 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2758 )) 2759 2760 if blockStart == blockEnd: 2761 uLogger.debug("Skipped this zero-length block...") 2762 2763 else: 2764 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2765 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2766 self.body = str({ 2767 "figi": self.figi, 2768 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2769 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2770 "interval": TKS_CANDLE_INTERVALS[interval][0] 2771 }) 2772 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2773 2774 if "code" in responseJSON.keys(): 2775 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2776 2777 else: 2778 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2779 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2780 2781 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2782 2783 blockEnd = blockStart 2784 2785 printCount = len(responseJSONs) # candles to show in console 2786 if responseJSONs: 2787 tempHistory = pd.DataFrame( 2788 data={ 2789 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2790 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2791 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2792 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2793 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2794 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2795 "volume": [int(item["volume"]) for item in responseJSONs], 2796 }, 2797 index=range(len(responseJSONs)), 2798 columns=["date", "time", "open", "high", "low", "close", "volume"], 2799 ) 2800 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2801 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2802 2803 # append only newest candles to old history if --only-missing key present: 2804 if onlyMissing and tempOld is not None and lastTime is not None: 2805 index = 0 # find start index in tempHistory data: 2806 2807 for i, item in tempHistory.iterrows(): 2808 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2809 2810 if curTime == lastTime: 2811 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2812 index = i 2813 printCount = index + 1 2814 break 2815 2816 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2817 2818 else: 2819 history = tempHistory # if no `--only-missing` key then load full data from server 2820 2821 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2822 2823 if history is not None and not history.empty: 2824 if show: 2825 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2826 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2827 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2828 )) 2829 2830 else: 2831 uLogger.warning("Received an empty candles history!") 2832 2833 if self.historyFile is not None: 2834 if history is not None and not history.empty: 2835 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2836 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2837 2838 else: 2839 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2840 2841 else: 2842 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2843 2844 return history 2845 2846 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2847 """ 2848 Load candles history from csv-file and return Pandas DataFrame object. 2849 2850 See also: `History()` and `ShowHistoryChart()` methods. 2851 2852 :param filePath: path to csv-file to open. 2853 """ 2854 loadedHistory = None # init candles data object 2855 2856 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2857 2858 if os.path.exists(filePath): 2859 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2860 2861 tfStr = self.priceModel.FormattedDelta( 2862 self.priceModel.timeframe, 2863 "{days} days {hours}h {minutes}m {seconds}s", 2864 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2865 self.priceModel.timeframe, 2866 "{hours}h {minutes}m {seconds}s", 2867 ) 2868 2869 if loadedHistory is not None and not loadedHistory.empty: 2870 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2871 len(loadedHistory), 2872 tfStr, 2873 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2874 ) 2875 2876 else: 2877 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2878 2879 else: 2880 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2881 2882 return loadedHistory 2883 2884 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2885 """ 2886 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2887 2888 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2889 Default: `index.html` (both for interact and non-interact candlesticks chart). 2890 2891 See also: `History()` and `LoadHistory()` methods. 2892 2893 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2894 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2895 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2896 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2897 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2898 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2899 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2900 """ 2901 if isinstance(candles, str): 2902 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2903 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2904 2905 elif isinstance(candles, pd.DataFrame): 2906 self.priceModel.prices = candles # set candles chain from variable 2907 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2908 2909 if "datetime" not in candles.columns: 2910 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2911 2912 else: 2913 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2914 raise Exception("Incorrect value") 2915 2916 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2917 2918 if interact: 2919 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2920 2921 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2922 2923 else: 2924 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2925 2926 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2927 2928 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2929 2930 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2931 """ 2932 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2933 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2934 2935 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2936 2937 :param operation: string "Buy" or "Sell". 2938 :param lots: volume, integer count of lots >= 1. 2939 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2940 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2941 :param expDate: string "Undefined" by default or local date in future, 2942 it is a string with format `%Y-%m-%d %H:%M:%S`. 2943 :return: JSON with response from broker server. 2944 """ 2945 if self.accountId is None or not self.accountId: 2946 uLogger.error("Variable `accountId` must be defined for using this method!") 2947 raise Exception("Account ID required") 2948 2949 if operation is None or not operation or operation not in ("Buy", "Sell"): 2950 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2951 raise Exception("Incorrect value") 2952 2953 if lots is None or lots < 1: 2954 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2955 lots = 1 2956 2957 if tp is None or tp < 0: 2958 tp = 0 2959 2960 if sl is None or sl < 0: 2961 sl = 0 2962 2963 if expDate is None or not expDate: 2964 expDate = "Undefined" 2965 2966 if not (self.ticker or self.figi): 2967 uLogger.error("Ticker or FIGI must be defined!") 2968 raise Exception("Ticker or FIGI required") 2969 2970 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2971 self.ticker = instrument["ticker"] 2972 self.figi = instrument["figi"] 2973 2974 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2975 2976 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2977 self.body = str({ 2978 "figi": self.figi, 2979 "quantity": str(lots), 2980 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2981 "accountId": str(self.accountId), 2982 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2983 }) 2984 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2985 2986 if "orderId" in response.keys(): 2987 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2988 operation, response["orderId"], 2989 self.ticker, self.figi, lots, 2990 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2991 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2992 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2993 )) 2994 2995 if tp > 0: 2996 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2997 2998 if sl > 0: 2999 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3000 3001 else: 3002 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3003 3004 return response 3005 3006 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3007 """ 3008 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3009 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3010 3011 See also: `Order()` and `Trade()` docstrings. 3012 3013 :param lots: volume, integer count of lots >= 1. 3014 :param tp: float > 0, take profit price of stop-order. 3015 :param sl: float > 0, stop loss price of stop-order. 3016 :param expDate: it's a local date in future. 3017 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3018 :return: JSON with response from broker server. 3019 """ 3020 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 3021 3022 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3023 """ 3024 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3025 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3026 3027 See also: `Order()` and `Trade()` docstrings. 3028 3029 :param lots: volume, integer count of lots >= 1. 3030 :param tp: float > 0, take profit price of stop-order. 3031 :param sl: float > 0, stop loss price of stop-order. 3032 :param expDate: it's a local date in the future. 3033 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3034 :return: JSON with response from broker server. 3035 """ 3036 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3037 3038 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3039 """ 3040 Close position of given instruments. 3041 3042 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3043 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3044 This avoids unnecessary downloading data from the server. 3045 """ 3046 if instruments is None or not instruments: 3047 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3048 raise Exception("Ticker or FIGI required") 3049 3050 if isinstance(instruments, str): 3051 instruments = [instruments] 3052 3053 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3054 if uniqueInstruments: 3055 if portfolio is None or not portfolio: 3056 portfolio = self.Overview(show=False) 3057 3058 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3059 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3060 3061 for self.figi in uniqueInstruments: 3062 if self.figi not in allOpened: 3063 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3064 continue 3065 3066 # search open trade info about instrument by ticker: 3067 instrument = {} 3068 for iType in TKS_INSTRUMENTS: 3069 if instrument: 3070 break 3071 3072 for item in portfolio["stat"][iType]: 3073 if item["figi"] == self.figi: 3074 instrument = item 3075 break 3076 3077 if instrument: 3078 self.ticker = instrument["ticker"] 3079 self.figi = instrument["figi"] 3080 3081 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3082 self.ticker, 3083 self.figi, 3084 int(instrument["volume"]), 3085 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3086 )) 3087 3088 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3089 3090 if tradeLots > 0: 3091 if instrument["blocked"] > 0: 3092 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3093 instrument["blocked"], 3094 self.ticker, 3095 tradeLots, 3096 )) 3097 3098 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3099 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3100 3101 else: 3102 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3103 3104 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3105 """ 3106 Close all positions of given instruments with defined type. 3107 3108 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3109 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3110 This avoids unnecessary downloading data from the server. 3111 """ 3112 if iType not in TKS_INSTRUMENTS: 3113 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3114 3115 else: 3116 if portfolio is None or not portfolio: 3117 portfolio = self.Overview(show=False) 3118 3119 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3120 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3121 3122 if tickers and portfolio: 3123 self.CloseTrades(tickers, portfolio) 3124 3125 else: 3126 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3127 3128 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3129 """ 3130 Universal method to create market or limit orders with all available parameters for current `accountId`. 3131 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3132 3133 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3134 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3135 3136 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3137 then broker immediately open market order as you can do simple --buy or --sell operations! 3138 3139 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3140 When current price will go up or down to target price value then broker opens a limit order. 3141 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3142 3143 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3144 3145 :param operation: string "Buy" or "Sell". 3146 :param orderType: string "Limit" or "Stop". 3147 :param lots: volume, integer count of lots >= 1. 3148 :param targetPrice: target price > 0. This is open trade price for limit order. 3149 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3150 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3151 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3152 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3153 Stop loss order always executed by market price. 3154 :param expDate: string "Undefined" by default or local date in future. 3155 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3156 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3157 A limit order has no expiration date, it lasts until the end of the trading day. 3158 :return: JSON with response from broker server. 3159 """ 3160 if self.accountId is None or not self.accountId: 3161 uLogger.error("Variable `accountId` must be defined for using this method!") 3162 raise Exception("Account ID required") 3163 3164 if operation is None or not operation or operation not in ("Buy", "Sell"): 3165 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3166 raise Exception("Incorrect value") 3167 3168 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3169 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3170 raise Exception("Incorrect value") 3171 3172 if lots is None or lots < 1: 3173 uLogger.error("You must define trade volume > 0: integer count of lots!") 3174 raise Exception("Incorrect value") 3175 3176 if targetPrice is None or targetPrice <= 0: 3177 uLogger.error("Target price for limit-order must be greater than 0!") 3178 raise Exception("Incorrect value") 3179 3180 if limitPrice is None or limitPrice <= 0: 3181 limitPrice = targetPrice 3182 3183 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3184 stopType = "Limit" 3185 3186 if expDate is None or not expDate: 3187 expDate = "Undefined" 3188 3189 if not (self.ticker or self.figi): 3190 uLogger.error("Tocker or FIGI must be defined!") 3191 raise Exception("Ticker or FIGI required") 3192 3193 response = {} 3194 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3195 self.ticker = instrument["ticker"] 3196 self.figi = instrument["figi"] 3197 3198 if orderType == "Limit": 3199 uLogger.debug( 3200 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3201 self.ticker, self.figi, 3202 operation, lots, targetPrice, instrument["currency"], 3203 )) 3204 3205 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3206 self.body = str({ 3207 "figi": self.figi, 3208 "quantity": str(lots), 3209 "price": FloatToNano(targetPrice), 3210 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3211 "accountId": str(self.accountId), 3212 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3213 }) 3214 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3215 3216 if "orderId" in response.keys(): 3217 uLogger.info( 3218 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3219 response["orderId"], 3220 self.ticker, self.figi, 3221 operation, lots, targetPrice, instrument["currency"], 3222 )) 3223 3224 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3225 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3226 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3227 targetPrice, instrument["currency"], 3228 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3229 )) 3230 3231 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3232 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 else: 3238 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3239 3240 if orderType == "Stop": 3241 uLogger.debug( 3242 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3243 self.ticker, self.figi, 3244 operation, lots, 3245 targetPrice, instrument["currency"], 3246 limitPrice, instrument["currency"], 3247 stopType, expDate, 3248 )) 3249 3250 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3251 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3252 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3253 3254 body = { 3255 "figi": self.figi, 3256 "quantity": str(lots), 3257 "price": FloatToNano(limitPrice), 3258 "stopPrice": FloatToNano(targetPrice), 3259 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3260 "accountId": str(self.accountId), 3261 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3262 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3263 } 3264 3265 if expDateUTC: 3266 body["expireDate"] = expDateUTC 3267 3268 self.body = str(body) 3269 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3270 3271 if "stopOrderId" in response.keys(): 3272 uLogger.info( 3273 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3274 response["stopOrderId"], 3275 self.ticker, self.figi, 3276 operation, lots, 3277 targetPrice, instrument["currency"], 3278 limitPrice, instrument["currency"], 3279 TKS_STOP_ORDER_TYPES[stopOrderType], 3280 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3281 )) 3282 3283 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3284 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3285 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3286 targetPrice, instrument["currency"], 3287 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3288 )) 3289 3290 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3291 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3292 targetPrice, instrument["currency"], 3293 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3294 )) 3295 3296 else: 3297 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3298 3299 return response 3300 3301 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3302 """ 3303 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3304 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3305 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3306 See also: `Order()` docstring. 3307 3308 :param lots: volume, integer count of lots >= 1. 3309 :param targetPrice: target price > 0. This is open trade price for limit order. 3310 :return: JSON with response from broker server. 3311 """ 3312 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3313 3314 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3315 """ 3316 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3317 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3318 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3319 target price value then broker opens a limit order. See also: `Order()` docstring. 3320 3321 :param lots: volume, integer count of lots >= 1. 3322 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3323 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3324 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3325 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3326 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3327 :param expDate: string "Undefined" by default or local date in future. 3328 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3329 This date is converting to UTC format for server. 3330 :return: JSON with response from broker server. 3331 """ 3332 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3333 3334 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3335 """ 3336 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3337 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3338 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3339 See also: `Order()` docstring. 3340 3341 :param lots: volume, integer count of lots >= 1. 3342 :param targetPrice: target price > 0. This is open trade price for limit order. 3343 :return: JSON with response from broker server. 3344 """ 3345 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3346 3347 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3348 """ 3349 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3350 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3351 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3352 target price value then broker opens a limit order. See also: `Order()` docstring. 3353 3354 :param lots: volume, integer count of lots >= 1. 3355 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3356 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3357 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3358 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3359 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3360 :param expDate: string "Undefined" by default or local date in future. 3361 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3362 This date is converting to UTC format for server. 3363 :return: JSON with response from broker server. 3364 """ 3365 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3366 3367 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3368 """ 3369 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3370 3371 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3372 :param allOrdersIDs: pre-received lists of all active pending orders. 3373 This avoids unnecessary downloading data from the server. 3374 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3375 """ 3376 if self.accountId is None or not self.accountId: 3377 uLogger.error("Variable `accountId` must be defined for using this method!") 3378 raise Exception("Account ID required") 3379 3380 if orderIDs: 3381 if allOrdersIDs is None or not allOrdersIDs: 3382 rawOrders = self.RequestPendingOrders() 3383 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3384 3385 if allStopOrdersIDs is None or not allStopOrdersIDs: 3386 rawStopOrders = self.RequestStopOrders() 3387 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3388 3389 for orderID in orderIDs: 3390 idInPendingOrders = orderID in allOrdersIDs 3391 idInStopOrders = orderID in allStopOrdersIDs 3392 3393 if not (idInPendingOrders or idInStopOrders): 3394 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3395 continue 3396 3397 else: 3398 if idInPendingOrders: 3399 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3400 3401 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3402 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3403 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3404 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3405 3406 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3407 if self.moreDebug: 3408 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3409 3410 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3411 3412 else: 3413 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3414 3415 elif idInStopOrders: 3416 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3417 3418 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3419 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3420 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3421 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3422 3423 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3424 if self.moreDebug: 3425 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3426 3427 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3428 3429 else: 3430 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3431 3432 else: 3433 continue 3434 3435 def CloseAllOrders(self) -> None: 3436 """ 3437 Gets a list of open pending and stop orders and cancel it all. 3438 """ 3439 rawOrders = self.RequestPendingOrders() 3440 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3441 lenOrders = len(allOrdersIDs) 3442 3443 rawStopOrders = self.RequestStopOrders() 3444 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3445 lenSOrders = len(allStopOrdersIDs) 3446 3447 if lenOrders > 0 or lenSOrders > 0: 3448 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3449 3450 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3451 3452 else: 3453 uLogger.info("Orders not found, nothing to cancel.") 3454 3455 def CloseAll(self, *args) -> None: 3456 """ 3457 Close all available (not blocked) opened trades and orders. 3458 3459 Also, you can select one or more keywords case-insensitive: 3460 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3461 3462 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3463 """ 3464 overview = self.Overview(show=False) # get all open trades info 3465 3466 if len(args) == 0: 3467 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3468 self.CloseAllOrders() # close all pending and stop orders 3469 3470 for iType in TKS_INSTRUMENTS: 3471 if iType != "Currencies": 3472 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3473 3474 else: 3475 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3476 lowerArgs = [x.lower() for x in args] 3477 3478 if "orders" in lowerArgs: 3479 self.CloseAllOrders() # close all pending and stop orders 3480 3481 for iType in TKS_INSTRUMENTS: 3482 if iType.lower() in lowerArgs and iType != "Currencies": 3483 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3484 3485 @staticmethod 3486 def ParseOrderParameters(operation, **inputParameters): 3487 """ 3488 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3489 3490 :param operation: string "Buy" or "Sell". 3491 :param inputParameters: this is dict of strings that looks like this 3492 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3493 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3494 "prices" key: one or more prices to open limit-orders 3495 Counts of values in lots and prices lists must be equals! 3496 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3497 """ 3498 # TODO: update order grid work with api v2 3499 pass 3500 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3501 # 3502 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3503 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3504 # raise Exception("Incorrect value") 3505 # 3506 # if "l" in inputParameters.keys(): 3507 # inputParameters["lots"] = inputParameters.pop("l") 3508 # 3509 # if "p" in inputParameters.keys(): 3510 # inputParameters["prices"] = inputParameters.pop("p") 3511 # 3512 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3513 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3514 # raise Exception("Incorrect value") 3515 # 3516 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3517 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3518 # 3519 # if len(lots) != len(prices): 3520 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3521 # raise Exception("Incorrect value") 3522 # 3523 # uLogger.debug("Extracted parameters for orders:") 3524 # uLogger.debug("lots = {}".format(lots)) 3525 # uLogger.debug("prices = {}".format(prices)) 3526 # 3527 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3528 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3529 # uLogger.debug("Order parameters: {}".format(result)) 3530 # 3531 # return result 3532 3533 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3534 """ 3535 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3536 3537 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3538 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3539 """ 3540 result = False 3541 msg = "Instrument not defined!" 3542 3543 if portfolio is None or not portfolio: 3544 portfolio = self.Overview(show=False) 3545 3546 if self.ticker: 3547 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3548 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3549 3550 for iType in TKS_INSTRUMENTS: 3551 for instrument in portfolio["stat"][iType]: 3552 if instrument["ticker"] == self.ticker: 3553 result = True 3554 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3555 break 3556 3557 elif self.figi: 3558 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3559 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3560 3561 for iType in TKS_INSTRUMENTS: 3562 for instrument in portfolio["stat"][iType]: 3563 if instrument["figi"] == self.figi: 3564 result = True 3565 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3566 break 3567 3568 else: 3569 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3570 3571 uLogger.debug(msg) 3572 3573 return result 3574 3575 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3576 """ 3577 Returns instrument is in the user's portfolio if it presents there. 3578 Instrument must be defined by `ticker` (highly priority) or `figi`. 3579 3580 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3581 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3582 """ 3583 result = None 3584 msg = "Instrument not defined!" 3585 3586 if portfolio is None or not portfolio: 3587 portfolio = self.Overview(show=False) 3588 3589 if self.ticker: 3590 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3591 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3592 3593 for iType in TKS_INSTRUMENTS: 3594 for instrument in portfolio["stat"][iType]: 3595 if instrument["ticker"] == self.ticker: 3596 result = instrument 3597 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3598 break 3599 3600 elif self.figi: 3601 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3602 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3603 3604 for iType in TKS_INSTRUMENTS: 3605 for instrument in portfolio["stat"][iType]: 3606 if instrument["figi"] == self.figi: 3607 result = instrument 3608 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3609 break 3610 3611 else: 3612 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3613 3614 uLogger.debug(msg) 3615 3616 return result 3617 3618 def RequestLimits(self) -> dict: 3619 """ 3620 Method for obtaining the available funds for withdrawal for current `accountId`. 3621 3622 See also: 3623 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3624 - `OverviewLimits()` method 3625 3626 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3627 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3628 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3629 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3630 """ 3631 if self.accountId is None or not self.accountId: 3632 uLogger.error("Variable `accountId` must be defined for using this method!") 3633 raise Exception("Account ID required") 3634 3635 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3636 3637 self.body = str({"accountId": self.accountId}) 3638 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3639 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3640 3641 if self.moreDebug: 3642 uLogger.debug("Records about available funds for withdrawal successfully received") 3643 3644 return rawLimits 3645 3646 def OverviewLimits(self, show: bool = False) -> dict: 3647 """ 3648 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3649 3650 See also: `RequestLimits()`. 3651 3652 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3653 :return: dict with raw parsed data from server and some calculated statistics about it. 3654 """ 3655 if self.accountId is None or not self.accountId: 3656 uLogger.error("Variable `accountId` must be defined for using this method!") 3657 raise Exception("Account ID required") 3658 3659 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3660 3661 view = { 3662 "rawLimits": rawLimits, 3663 "limits": { # parsed data for every currency: 3664 "money": { # this is an array of portfolio currency positions 3665 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3666 }, 3667 "blocked": { # this is an array of blocked currency 3668 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3669 }, 3670 "blockedGuarantee": { # this is locked money under collateral for futures 3671 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3672 }, 3673 }, 3674 } 3675 3676 # --- Prepare text table with limits in human-readable format: 3677 if show: 3678 info = [ 3679 "# Withdrawal limits\n\n", 3680 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3681 "* **Account ID:** [{}]\n".format(self.accountId), 3682 ] 3683 3684 if view["limits"]["money"]: 3685 info.extend([ 3686 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3687 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3688 ]) 3689 3690 else: 3691 info.append("\nNo withdrawal limits\n") 3692 3693 for curr in view["limits"]["money"].keys(): 3694 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3695 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3696 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3697 3698 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3699 "[{}]".format(curr), 3700 "{:.2f}".format(view["limits"]["money"][curr]), 3701 "{:.2f}".format(availableMoney), 3702 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3703 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3704 ) 3705 3706 if curr == "rub": 3707 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3708 3709 else: 3710 info.append(infoStr) 3711 3712 infoText = "".join(info) 3713 3714 uLogger.info(infoText) 3715 3716 if self.withdrawalLimitsFile: 3717 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3718 fH.write(infoText) 3719 3720 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3721 3722 return view 3723 3724 def RequestAccounts(self) -> dict: 3725 """ 3726 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3727 3728 See also: 3729 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3730 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3731 - `OverviewUserInfo()` method 3732 3733 :return: dict with raw data from server that contains accounts info. Example of dict: 3734 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3735 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3736 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3737 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3738 """ 3739 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3740 3741 self.body = str({}) 3742 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3743 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3744 3745 if self.moreDebug: 3746 uLogger.debug("Records about available accounts successfully received") 3747 3748 return rawAccounts 3749 3750 def RequestUserInfo(self) -> dict: 3751 """ 3752 Method for requesting common user's information. 3753 3754 See also: 3755 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3756 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3757 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3758 - `OverviewUserInfo()` method 3759 3760 :return: dict with raw data from server that contains user's information. Example of dict: 3761 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3762 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3763 """ 3764 uLogger.debug("Requesting common user's information. Wait, please...") 3765 3766 self.body = str({}) 3767 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3768 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3769 3770 if self.moreDebug: 3771 uLogger.debug("Records about current user successfully received") 3772 3773 return rawUserInfo 3774 3775 def RequestMarginStatus(self, accountId: str = None) -> dict: 3776 """ 3777 Method for requesting margin calculation for defined account ID. 3778 3779 See also: 3780 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3781 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3782 - `OverviewUserInfo()` method 3783 3784 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3785 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3786 Example of responses: 3787 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3788 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3789 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3790 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3791 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3792 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3793 """ 3794 if accountId is None or not accountId: 3795 if self.accountId is None or not self.accountId: 3796 uLogger.error("Variable `accountId` must be defined for using this method!") 3797 raise Exception("Account ID required") 3798 3799 else: 3800 accountId = self.accountId # use `self.accountId` (main ID) by default 3801 3802 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3803 3804 self.body = str({"accountId": accountId}) 3805 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3806 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3807 3808 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3809 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3810 rawMargin = {} 3811 3812 else: 3813 if self.moreDebug: 3814 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3815 3816 return rawMargin 3817 3818 def RequestTariffLimits(self) -> dict: 3819 """ 3820 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3821 3822 See also: 3823 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3824 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3825 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3826 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3827 - `OverviewUserInfo()` method 3828 3829 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3830 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3831 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3832 """ 3833 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3834 3835 self.body = str({}) 3836 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3837 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3838 3839 if self.moreDebug: 3840 uLogger.debug("Records with limits of current tariff successfully received") 3841 3842 return rawTariffLimits 3843 3844 def RequestBondCoupons(self, iJSON: dict) -> dict: 3845 """ 3846 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3847 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3848 All dates are in UTC timezone. 3849 3850 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3851 Documentation: 3852 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3853 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3854 3855 See also: `ExtendBondsData()`. 3856 3857 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3858 If raw iJSON is not data of bond then server returns an error [400] with message: 3859 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3860 :return: dictionary with bond payment calendar. Response example 3861 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3862 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3863 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3864 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3865 """ 3866 if iJSON["figi"] is None or not iJSON["figi"]: 3867 uLogger.error("FIGI must be defined for using this method!") 3868 raise Exception("FIGI required") 3869 3870 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3871 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3872 3873 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3874 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3875 self.figi, 3876 startDate, 3877 endDate, 3878 )) 3879 3880 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3881 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3882 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3883 3884 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3885 uLogger.warning("Instrument type is not bond!") 3886 3887 else: 3888 if self.moreDebug: 3889 uLogger.debug("Records about bond payment calendar successfully received") 3890 3891 return calendar 3892 3893 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3894 """ 3895 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3896 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3897 coupon yields, current yields and some statistics etc. 3898 3899 WARNING! This is too long operation if a lot of bonds requested from broker server. 3900 3901 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3902 3903 :param instruments: list of strings with tickers or FIGIs. 3904 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3905 for further used by data scientists or stock analytics. 3906 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3907 In XLSX-file and Pandas DataFrame fields mean: 3908 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3909 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3910 """ 3911 if instruments is None or not instruments: 3912 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3913 raise Exception("Ticker or FIGI required") 3914 3915 if isinstance(instruments, str): 3916 instruments = [instruments] 3917 3918 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3919 3920 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3921 3922 iCount = len(uniqueInstruments) 3923 tooLong = iCount >= 20 3924 if tooLong: 3925 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3926 3927 bonds = None 3928 for i, self.figi in enumerate(uniqueInstruments): 3929 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3930 3931 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3932 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3933 rawBond = self.SearchByFIGI(requestPrice=True) 3934 3935 # Widen raw data with UTC current time (iData["actualDateTime"]): 3936 actualDate = datetime.now(tzutc()) 3937 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3938 3939 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3940 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3941 3942 # Replace some values with human-readable: 3943 iData["nominalCurrency"] = iData["nominal"]["currency"] 3944 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3945 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3946 iData["aciCurrency"] = iData["aciValue"]["currency"] 3947 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3948 iData["issueSize"] = int(iData["issueSize"]) 3949 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3950 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3951 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3952 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3953 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3954 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3955 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3956 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3957 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3958 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3959 3960 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3961 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3962 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3963 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3964 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3965 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3966 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3967 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3968 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3969 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3970 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3971 3972 # Widen raw data with calendar data from `rawCalendar` values: 3973 calendarData = [] 3974 if "events" in iData["rawCalendar"].keys(): 3975 for item in iData["rawCalendar"]["events"]: 3976 calendarData.append({ 3977 "couponDate": item["couponDate"], 3978 "couponNumber": int(item["couponNumber"]), 3979 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3980 "payCurrency": item["payOneBond"]["currency"], 3981 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3982 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3983 "couponStartDate": item["couponStartDate"], 3984 "couponEndDate": item["couponEndDate"], 3985 "couponPeriod": item["couponPeriod"], 3986 }) 3987 3988 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3989 if "maturityDate" not in iData.keys(): 3990 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3991 3992 # Widen raw data with Coupon Rate. 3993 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3994 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3995 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3996 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3997 3998 # Widen raw data with Yield to Maturity (YTM) on current date. 3999 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4000 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4001 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4002 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4003 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4004 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4005 4006 iData["calendar"] = calendarData # adds calendar at the end 4007 4008 # Remove not used data: 4009 iData.pop("uid") 4010 iData.pop("positionUid") 4011 iData.pop("currentPrice") 4012 iData.pop("rawCalendar") 4013 4014 colNames = list(iData.keys()) 4015 if bonds is None: 4016 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4017 4018 else: 4019 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4020 4021 else: 4022 uLogger.warning("Instrument is not a bond!") 4023 4024 processed = round(100 * (i + 1) / iCount, 1) 4025 if tooLong and processed % 5 == 0: 4026 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4027 4028 else: 4029 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4030 4031 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4032 4033 # Saving bonds from Pandas DataFrame to XLSX sheet: 4034 if xlsx and self.bondsXLSXFile: 4035 with pd.ExcelWriter( 4036 path=self.bondsXLSXFile, 4037 date_format=TKS_DATE_FORMAT, 4038 datetime_format=TKS_DATE_TIME_FORMAT, 4039 mode="w", 4040 ) as writer: 4041 bonds.to_excel( 4042 writer, 4043 sheet_name="Extended bonds data", 4044 index=True, 4045 encoding="UTF-8", 4046 freeze_panes=(1, 1), 4047 ) # saving as XLSX-file with freeze first row and column as headers 4048 4049 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4050 4051 return bonds 4052 4053 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4054 """ 4055 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4056 4057 WARNING! This is too long operation if a lot of bonds requested from broker server. 4058 4059 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4060 4061 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4062 extended information about bonds: main info, current prices, bond payment calendar, 4063 coupon yields, current yields and some statistics etc. 4064 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4065 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4066 for further used by data scientists or stock analytics. 4067 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4068 """ 4069 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4070 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4071 4072 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4073 4074 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4075 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4076 calendar = None 4077 for bond in extBonds.iterrows(): 4078 for item in bond[1]["calendar"]: 4079 cData = { 4080 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4081 "couponDate": item["couponDate"], 4082 "figi": bond[1]["figi"], 4083 "ticker": bond[1]["ticker"], 4084 "name": bond[1]["name"], 4085 "couponNumber": item["couponNumber"], 4086 "payOneBond": item["payOneBond"], 4087 "payCurrency": item["payCurrency"], 4088 "couponType": item["couponType"], 4089 "couponPeriod": item["couponPeriod"], 4090 "fixDate": item["fixDate"], 4091 "couponStartDate": item["couponStartDate"], 4092 "couponEndDate": item["couponEndDate"], 4093 } 4094 4095 if calendar is None: 4096 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4097 4098 else: 4099 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4100 4101 if calendar is not None: 4102 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4103 4104 # Saving calendar from Pandas DataFrame to XLSX sheet: 4105 if xlsx: 4106 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4107 4108 with pd.ExcelWriter( 4109 path=xlsxCalendarFile, 4110 date_format=TKS_DATE_FORMAT, 4111 datetime_format=TKS_DATE_TIME_FORMAT, 4112 mode="w", 4113 ) as writer: 4114 humanReadable = calendar.copy(deep=True) 4115 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4116 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4117 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4118 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4119 humanReadable.columns = colNames # human-readable column names 4120 4121 humanReadable.to_excel( 4122 writer, 4123 sheet_name="Bond payments calendar", 4124 index=False, 4125 encoding="UTF-8", 4126 freeze_panes=(1, 2), 4127 ) # saving as XLSX-file with freeze first row and column as headers 4128 4129 del humanReadable # release df in memory 4130 4131 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4132 4133 return calendar 4134 4135 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4136 """ 4137 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4138 Also, creates Markdown file with calendar data, `calendar.md` by default. 4139 4140 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4141 4142 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4143 extended information about bonds: main info, current prices, bond payment calendar, 4144 coupon yields, current yields and some statistics etc. 4145 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4146 :param show: if `True` then also printing bonds payment calendar to the console, 4147 otherwise save to file `calendarFile` only. `False` by default. 4148 :return: multilines text in Markdown format with bonds payment calendar as a table. 4149 """ 4150 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4151 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4152 4153 infoText = "# Bond payments calendar\n\n" 4154 4155 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4156 4157 if not (calendar is None or calendar.empty): 4158 splitLine = "| | | | | | | | | |\n" 4159 4160 info = [ 4161 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4162 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4163 ] 4164 4165 newMonth = False 4166 notOneBond = calendar["figi"].nunique() > 1 4167 for i, bond in enumerate(calendar.iterrows()): 4168 if newMonth and notOneBond: 4169 info.append(splitLine) 4170 4171 info.append( 4172 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4173 " √" if bond[1]["paid"] else " —", 4174 bond[1]["couponDate"].split("T")[0], 4175 bond[1]["figi"], 4176 bond[1]["ticker"], 4177 bond[1]["couponNumber"], 4178 "{} {}".format( 4179 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4180 bond[1]["payCurrency"], 4181 ), 4182 bond[1]["couponType"], 4183 bond[1]["couponPeriod"], 4184 bond[1]["fixDate"].split("T")[0], 4185 ) 4186 ) 4187 4188 if i < len(calendar.values) - 1: 4189 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4190 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4191 newMonth = False if curDate.month == nextDate.month else True 4192 4193 else: 4194 newMonth = False 4195 4196 infoText += "".join(info) 4197 4198 if show: 4199 uLogger.info("{}".format(infoText)) 4200 4201 if self.calendarFile is not None: 4202 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4203 fH.write(infoText) 4204 4205 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4206 4207 else: 4208 infoText += "No data\n" 4209 4210 return infoText 4211 4212 def OverviewAccounts(self, show: bool = False) -> dict: 4213 """ 4214 Method for parsing and show simple table with all available user accounts. 4215 4216 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4217 4218 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4219 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4220 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4221 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4222 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4223 "closed": "—", "access": "Full access" }, ...}}` 4224 """ 4225 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4226 4227 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4228 accounts = { 4229 item["id"]: { 4230 "type": TKS_ACCOUNT_TYPES[item["type"]], 4231 "name": item["name"], 4232 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4233 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4234 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4235 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4236 } for item in rawAccounts["accounts"] 4237 } 4238 4239 # Raw and parsed data with some fields replaced in "stat" section: 4240 view = { 4241 "rawAccounts": rawAccounts, 4242 "stat": accounts, 4243 } 4244 4245 # --- Prepare simple text table with only accounts data in human-readable format: 4246 if show: 4247 info = [ 4248 "# User accounts\n\n", 4249 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4250 "| Account ID | Type | Status | Name |\n", 4251 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4252 ] 4253 4254 for account in view["stat"].keys(): 4255 info.extend([ 4256 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4257 account, 4258 view["stat"][account]["type"], 4259 view["stat"][account]["status"], 4260 view["stat"][account]["name"], 4261 ) 4262 ]) 4263 4264 infoText = "".join(info) 4265 4266 uLogger.info(infoText) 4267 4268 if self.userAccountsFile: 4269 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4270 fH.write(infoText) 4271 4272 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4273 4274 return view 4275 4276 def OverviewUserInfo(self, show: bool = False) -> dict: 4277 """ 4278 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4279 4280 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4281 4282 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4283 :return: dict with raw parsed data from server and some calculated statistics about it. 4284 """ 4285 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4286 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4287 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4288 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4289 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4290 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4291 4292 # This is dict with parsed common user data: 4293 userInfo = { 4294 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4295 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4296 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4297 "tariff": rawUserInfo["tariff"], 4298 } 4299 4300 # This is an array of dict with parsed margin statuses for every account IDs: 4301 margins = {} 4302 for accountId in accounts.keys(): 4303 if rawMargins[accountId]: 4304 margins[accountId] = { 4305 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4306 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4307 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4308 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4309 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4310 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4311 } 4312 4313 else: 4314 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4315 4316 unary = {} # unary-connection limits 4317 for item in rawTariffLimits["unaryLimits"]: 4318 if item["limitPerMinute"] in unary.keys(): 4319 unary[item["limitPerMinute"]].extend(item["methods"]) 4320 4321 else: 4322 unary[item["limitPerMinute"]] = item["methods"] 4323 4324 stream = {} # stream-connection limits 4325 for item in rawTariffLimits["streamLimits"]: 4326 if item["limit"] in stream.keys(): 4327 stream[item["limit"]].extend(item["streams"]) 4328 4329 else: 4330 stream[item["limit"]] = item["streams"] 4331 4332 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4333 limits = { 4334 "unary": unary, 4335 "stream": stream, 4336 } 4337 4338 # Raw and parsed data as an output result: 4339 view = { 4340 "rawUserInfo": rawUserInfo, 4341 "rawAccounts": rawAccounts, 4342 "rawMargins": rawMargins, 4343 "rawTariffLimits": rawTariffLimits, 4344 "stat": { 4345 "userInfo": userInfo, 4346 "accounts": accounts, 4347 "margins": margins, 4348 "limits": limits, 4349 }, 4350 } 4351 4352 # --- Prepare text table with user information in human-readable format: 4353 if show: 4354 info = [ 4355 "# Full user information\n\n", 4356 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4357 "## Common information\n\n", 4358 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4359 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4360 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4361 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4362 "\n## User accounts\n\n", 4363 ] 4364 4365 for account in view["stat"]["accounts"].keys(): 4366 info.extend([ 4367 "### ID: [{}]\n\n".format(account), 4368 "| Parameters | Values |\n", 4369 "|----------------------|--------------------------------------------------------------|\n", 4370 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4371 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4372 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4373 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4374 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4375 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4376 ]) 4377 4378 if margins[account]: 4379 info.extend([ 4380 "| Margin status: | Enabled |\n", 4381 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4382 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4383 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4384 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4385 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4386 ]) 4387 4388 else: 4389 info.append("| Margin status: | Disabled |\n\n") 4390 4391 info.extend([ 4392 "\n## Current user tariff limits\n", 4393 "\nSee also:\n", 4394 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4395 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4396 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4397 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4398 "\n### Unary limits\n", 4399 ]) 4400 4401 if unary: 4402 for key, values in sorted(unary.items()): 4403 info.append("\n* Max requests per minute: {}\n".format(key)) 4404 4405 for value in values: 4406 info.append(" - {}\n".format(value)) 4407 4408 else: 4409 info.append("\nNot available\n") 4410 4411 info.append("\n### Stream limits\n") 4412 4413 if stream: 4414 for key, values in sorted(stream.items()): 4415 info.append("\n* Max stream connections: {}\n".format(key)) 4416 4417 for value in values: 4418 info.append(" - {}\n".format(value)) 4419 4420 else: 4421 info.append("\nNot available\n") 4422 4423 infoText = "".join(info) 4424 4425 uLogger.info(infoText) 4426 4427 if self.userInfoFile: 4428 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4429 fH.write(infoText) 4430 4431 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4432 4433 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.moreDebug = False 301 """Enables more debug information in this class, such as net request and response headers in all methods. `False` by default.""" 302 303 self.historyFile = None 304 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 305 306 See also: `History()`. 307 """ 308 309 self.htmlHistoryFile = "index.html" 310 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 311 312 See also: `ShowHistoryChart()`. 313 """ 314 315 self.instrumentsFile = "instruments.md" 316 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 317 318 See also: `ShowInstrumentsInfo()`. 319 """ 320 321 self.searchResultsFile = "search-results.md" 322 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 323 324 See also: `SearchInstruments()`. 325 """ 326 327 self.pricesFile = "prices.md" 328 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 329 330 See also: `GetListOfPrices()`. 331 """ 332 333 self.infoFile = "info.md" 334 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 335 336 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 337 """ 338 339 self.bondsXLSXFile = "ext-bonds.xlsx" 340 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 341 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 342 343 See also: `ExtendBondsData()`. 344 """ 345 346 self.calendarFile = "calendar.md" 347 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 348 349 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 350 351 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 352 """ 353 354 self.overviewFile = "overview.md" 355 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 356 357 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 358 """ 359 360 self.overviewDigestFile = "overview-digest.md" 361 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 362 363 See also: `Overview()` with parameter `details="digest"`. 364 """ 365 366 self.overviewPositionsFile = "overview-positions.md" 367 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 368 369 See also: `Overview()` with parameter `details="positions"`. 370 """ 371 372 self.overviewOrdersFile = "overview-orders.md" 373 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 374 375 See also: `Overview()` with parameter `details="orders"`. 376 """ 377 378 self.overviewAnalyticsFile = "overview-analytics.md" 379 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 380 381 See also: `Overview()` with parameter `details="analytics"`. 382 """ 383 384 self.overviewBondsCalendarFile = "overview-calendar.md" 385 """Filename where only the bonds calendar section will be saved. Default: `overview-calendar.md`. 386 387 See also: `Overview()` with parameter `details="calendar"`. 388 """ 389 390 self.reportFile = "deals.md" 391 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 392 393 See also: `Deals()`. 394 """ 395 396 self.withdrawalLimitsFile = "limits.md" 397 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 398 399 See also: `OverviewLimits()` and `RequestLimits()`. 400 """ 401 402 self.userInfoFile = "user-info.md" 403 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 404 405 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 406 """ 407 408 self.userAccountsFile = "accounts.md" 409 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 410 411 See also: `OverviewAccounts()`, `RequestAccounts()`. 412 """ 413 414 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 415 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 416 417 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 418 419 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 420 """ 421 422 self.iList = None # init iList for raw instruments data 423 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 424 425 See also: `Listing()`, `DumpInstruments()`. 426 """ 427 428 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 429 if useCache: 430 if os.path.exists(self.iListDumpFile): 431 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 432 curTime = datetime.now(tzutc()) 433 434 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 435 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 436 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 438 439 else: 440 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 441 442 uLogger.debug("Local cache with raw instruments data is used: [{}]. Last modified: [{}] UTC".format( 443 os.path.abspath(self.iListDumpFile), 444 dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT), 445 )) 446 447 else: 448 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 449 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 450 451 else: 452 self.iList = self.Listing() # request new raw instruments data from broker server 453 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 454 455 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 456 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 457 458 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 459 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Enables more debug information in this class, such as net request and response headers in all methods. False by default.
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where only the bonds calendar section will be saved. Default: overview-calendar.md.
See also: Overview() with parameter details="calendar".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
475 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5) -> dict: 476 """ 477 Send GET or POST request to broker server and receive JSON object. 478 479 self.header: must be defining with dictionary of headers. 480 self.body: if define then used as request body. None by default. 481 self.timeout: global request timeout, 15 seconds by default. 482 :param url: url with REST request. 483 :param reqType: send "GET" or "POST" request. "GET" by default. 484 :param retry: how many times retry after first request if an 5xx server errors occurred. 485 :param pause: sleep time in seconds between retries. 486 :return: response JSON (dictionary) from broker. 487 """ 488 if reqType not in ("GET", "POST"): 489 uLogger.error("You can define request type: 'GET' or 'POST'!") 490 raise Exception("Incorrect value") 491 492 if self.moreDebug: 493 uLogger.debug("Request parameters:") 494 uLogger.debug(" - REST API URL: {}".format(url)) 495 uLogger.debug(" - request type: {}".format(reqType)) 496 uLogger.debug(" - headers:\n{}".format(str(self.headers).replace(self.token, "*** request token ***"))) 497 uLogger.debug(" - body:\n{}".format(self.body)) 498 499 # fast hack to avoid all operations with some tickers/FIGI 500 responseJSON = {} 501 oK = True 502 for item in self.exclude: 503 if item in url: 504 if self.moreDebug: 505 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 506 507 oK = False 508 break 509 510 if oK: 511 counter = 0 512 response = None 513 errMsg = "" 514 515 while not response and counter <= retry: 516 if reqType == "GET": 517 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if reqType == "POST": 520 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 521 522 if self.moreDebug: 523 uLogger.debug("Response:") 524 uLogger.debug(" - status code: {}".format(response.status_code)) 525 uLogger.debug(" - reason: {}".format(response.reason)) 526 uLogger.debug(" - body length: {}".format(len(response.text))) 527 uLogger.debug(" - headers:\n{}".format(response.headers)) 528 529 # Server returns some headers: 530 # - `x-ratelimit-limit` — shows the settings of the current user limit for this method. 531 # - `x-ratelimit-remaining` — the number of remaining requests of this type per minute. 532 # - `x-ratelimit-reset` — time in seconds before resetting the request counter. 533 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 534 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 535 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 536 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 537 sleep(rateLimitWait) 538 539 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 540 if 400 <= response.status_code < 500: 541 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 542 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 543 counter = retry + 1 544 545 if 500 <= response.status_code < 600: 546 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 547 uLogger.debug(" - not oK, {}".format(errMsg)) 548 counter += 1 549 550 if counter <= retry: 551 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 552 sleep(pause) 553 554 responseJSON = self._ParseJSON(rawData=response.text) 555 556 if errMsg: 557 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 558 uLogger.error(" - not oK, {}".format(errMsg)) 559 560 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
Returns
response JSON (dictionary) from broker.
593 def Listing(self) -> dict: 594 """ 595 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 596 597 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 598 """ 599 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 600 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 601 602 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 603 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 604 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 605 606 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 607 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 608 poolUpdater.close() 609 610 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 611 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 612 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 613 614 # calculate minimum price increment (step) for all instruments and set up instrument's type: 615 for iType in iList.keys(): 616 for ticker in iList[iType]: 617 iList[iType][ticker]["type"] = iType 618 619 if "minPriceIncrement" in iList[iType][ticker].keys(): 620 iList[iType][ticker]["step"] = NanoToFloat( 621 iList[iType][ticker]["minPriceIncrement"]["units"], 622 iList[iType][ticker]["minPriceIncrement"]["nano"], 623 ) 624 625 else: 626 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 627 628 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
630 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 631 """ 632 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 633 634 See also: `DumpInstruments()`, `Listing()`. 635 636 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 637 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 638 """ 639 if self.iListDumpFile is None or not self.iListDumpFile: 640 uLogger.error("Output name of dump file must be defined!") 641 raise Exception("Filename required") 642 643 if not self.iList or forceUpdate: 644 self.iList = self.Listing() 645 646 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 647 648 # Save as XLSX with separated sheets for every type of instruments: 649 with pd.ExcelWriter( 650 path=xlsxDumpFile, 651 date_format=TKS_DATE_FORMAT, 652 datetime_format=TKS_DATE_TIME_FORMAT, 653 mode="w", 654 ) as writer: 655 for iType in TKS_INSTRUMENTS: 656 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 657 df = df[sorted(df)] # sorted by column names 658 df = df.applymap( 659 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 660 na_action="ignore", 661 ) # converting numbers from nano-type to float in every cell 662 df.to_excel( 663 writer, 664 sheet_name=iType, 665 encoding="UTF-8", 666 freeze_panes=(1, 1), 667 ) # saving as XLSX-file with freeze first row and column as headers 668 669 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
671 def DumpInstruments(self, forceUpdate: bool = True) -> str: 672 """ 673 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 674 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 675 676 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 677 678 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 679 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 680 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 681 """ 682 if self.iListDumpFile is None or not self.iListDumpFile: 683 uLogger.error("Output name of dump file must be defined!") 684 raise Exception("Filename required") 685 686 if not self.iList or forceUpdate: 687 self.iList = self.Listing() 688 689 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 690 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 691 fH.write(jsonDump) 692 693 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 694 695 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
697 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 698 """ 699 Show information about one instrument defined by json data and prints it in Markdown format. 700 701 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 702 703 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 704 :param show: if `True` then also printing information about instrument and its current price. 705 :return: multilines text in Markdown format with information about one instrument. 706 """ 707 splitLine = "| | |\n" 708 infoText = "" 709 710 if iJSON is not None and iJSON and isinstance(iJSON, dict): 711 info = [ 712 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 713 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 714 "| Parameters | Values |\n", 715 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 716 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 717 "| Full name: | {:<54} |\n".format(iJSON["name"]), 718 ] 719 720 if "sector" in iJSON.keys() and iJSON["sector"]: 721 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 722 723 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 724 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 725 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 726 ))) 727 728 info.extend([ 729 splitLine, 730 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 731 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 732 ]) 733 734 if "isin" in iJSON.keys() and iJSON["isin"]: 735 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 736 737 if "classCode" in iJSON.keys(): 738 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 739 740 info.extend([ 741 splitLine, 742 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 743 splitLine, 744 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 745 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 746 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 747 ]) 748 749 if iJSON["figi"]: 750 self.figi = iJSON["figi"] 751 iJSON = iJSON | self.RequestTradingStatus() 752 753 info.extend([ 754 splitLine, 755 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 756 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 757 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 758 ]) 759 760 info.append(splitLine) 761 762 if "type" in iJSON.keys() and iJSON["type"]: 763 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 764 765 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 766 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 767 768 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 769 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 770 771 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 772 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 773 774 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 775 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 776 777 if "focusType" in iJSON.keys() and iJSON["focusType"]: 778 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 779 780 if "assetType" in iJSON.keys() and iJSON["assetType"]: 781 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 782 783 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 784 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 785 786 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 787 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 788 789 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 790 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 791 792 if "currency" in iJSON.keys(): 793 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 794 795 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 796 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 797 798 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 799 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 802 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 803 804 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 805 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 806 807 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 808 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 809 810 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 811 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 812 813 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 814 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 815 816 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 817 info.append("| Perpetual bond: | Yes |\n") 818 819 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 820 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 821 822 iExt = None 823 if iJSON["type"] == "Bonds": 824 info.extend([ 825 splitLine, 826 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 827 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 828 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 829 iJSON["nominal"]["currency"], 830 )), 831 ]) 832 833 if "floatingCouponFlag" in iJSON.keys(): 834 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 835 836 if "amortizationFlag" in iJSON.keys(): 837 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 838 839 info.append(splitLine) 840 841 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 842 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 843 844 if iJSON["figi"]: 845 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 846 847 info.extend([ 848 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 849 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 850 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 851 ]) 852 853 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 854 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 855 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 856 iJSON["aciValue"]["currency"] 857 ))) 858 859 if "currentPrice" in iJSON.keys(): 860 info.append(splitLine) 861 862 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 863 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 864 865 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 866 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 867 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 868 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 869 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 870 871 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 872 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 873 874 info.extend([ 875 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 880 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 881 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 882 )), 883 "| Changes between last deal price and last close | {:<54} |\n".format( 884 "{:.2f}%{}".format( 885 iJSON["currentPrice"]["changes"], 886 " ({}{:.2f} {})".format( 887 "+" if bondChangesDelta > 0 else "", 888 bondChangesDelta, 889 aciCurrency 890 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 891 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 892 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 893 currency 894 ), 895 ) 896 ), 897 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 898 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 899 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 900 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 901 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 902 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 903 )), 904 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 905 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 906 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 907 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 908 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 909 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 910 )), 911 ]) 912 913 if "lot" in iJSON.keys(): 914 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 915 916 if "step" in iJSON.keys() and iJSON["step"] != 0: 917 info.append("| Minimum price increment (step): | {:<54} |\n".format("{} {}".format(iJSON["step"], iJSON["currency"] if "currency" in iJSON.keys() else ""))) 918 919 # Add bond payment calendar: 920 if iJSON["type"] == "Bonds": 921 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 922 info.extend(["\n", strCalendar]) 923 924 infoText += "".join(info) 925 926 if show: 927 uLogger.info("{}".format(infoText)) 928 929 else: 930 uLogger.debug("{}".format(infoText)) 931 932 if self.infoFile is not None: 933 with open(self.infoFile, "w", encoding="UTF-8") as fH: 934 fH.write(infoText) 935 936 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 937 938 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
940 def SearchByTicker(self, requestPrice: bool = False, show: bool = False) -> dict: 941 """ 942 Search and return raw broker's information about instrument by its ticker. Variable `ticker` must be defined! 943 944 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 945 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 946 :return: JSON formatted data with information about instrument. 947 """ 948 tickerJSON = {} 949 if self.moreDebug: 950 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 951 952 if not self.ticker: 953 uLogger.warning("self.ticker variable is not be empty!") 954 955 else: 956 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 957 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 958 raise Exception("Instrument not allowed") 959 960 if not self.iList: 961 self.iList = self.Listing() 962 963 if self.ticker in self.iList["Shares"].keys(): 964 tickerJSON = self.iList["Shares"][self.ticker] 965 if self.moreDebug: 966 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 967 968 elif self.ticker in self.iList["Currencies"].keys(): 969 tickerJSON = self.iList["Currencies"][self.ticker] 970 if self.moreDebug: 971 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 972 973 elif self.ticker in self.iList["Bonds"].keys(): 974 tickerJSON = self.iList["Bonds"][self.ticker] 975 if self.moreDebug: 976 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 977 978 elif self.ticker in self.iList["Etfs"].keys(): 979 tickerJSON = self.iList["Etfs"][self.ticker] 980 if self.moreDebug: 981 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 982 983 elif self.ticker in self.iList["Futures"].keys(): 984 tickerJSON = self.iList["Futures"][self.ticker] 985 if self.moreDebug: 986 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 987 988 if tickerJSON: 989 self.figi = tickerJSON["figi"] 990 991 if requestPrice: 992 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 993 994 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 995 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 996 997 else: 998 tickerJSON["currentPrice"]["changes"] = 0 999 1000 if show: 1001 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1002 1003 else: 1004 if show: 1005 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1006 1007 return tickerJSON
Search and return raw broker's information about instrument by its ticker. Variable ticker must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1009 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False) -> dict: 1010 """ 1011 Search and return raw broker's information about instrument by its FIGI. Variable `figi` must be defined! 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if self.moreDebug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if self.moreDebug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if self.moreDebug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if self.moreDebug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if self.moreDebug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if self.moreDebug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON
Search and return raw broker's information about instrument by its FIGI. Variable figi must be defined!
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console.
Returns
JSON formatted data with information about instrument.
1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1106 `{"buy": [{"price": 1243.8, "quantity": 193}, 1107 {"price": 1244.0, "quantity": 168}, 1108 {"price": 1244.8, "quantity": 5}, 1109 {"price": 1245.0, "quantity": 61}, 1110 {"price": 1245.4, "quantity": 60}], 1111 "sell": [{"price": 1243.6, "quantity": 8}, 1112 {"price": 1242.6, "quantity": 10}, 1113 {"price": 1242.4, "quantity": 18}, 1114 {"price": 1242.2, "quantity": 50}, 1115 {"price": 1242.0, "quantity": 113}], 1116 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1117 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1118 - sell: list of dicts with Buyers prices, 1119 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1120 - quantity: volume value by current price in lots, 1121 - limitUp: current trade session limit price, maximum, 1122 - limitDown: current trade session limit price, minimum, 1123 - lastPrice: last deal price of the instrument, 1124 - closePrice: previous trade session close price of the instrument. 1125 1126 See also: `SearchByTicker()` and `SearchByFIGI()`. 1127 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1128 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1129 1130 :param show: if `True` then print DOM to log and console. 1131 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1132 If an error occurred then returns an empty record: 1133 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1134 """ 1135 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1136 1137 if self.depth < 1: 1138 uLogger.error("Depth of Market (DOM) must be >=1!") 1139 raise Exception("Incorrect value") 1140 1141 if not (self.ticker or self.figi): 1142 uLogger.error("self.ticker or self.figi variables must be defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 if self.ticker and not self.figi: 1146 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1147 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1148 1149 if not self.ticker and self.figi: 1150 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1152 1153 if not self.figi: 1154 uLogger.error("FIGI is not defined!") 1155 raise Exception("Ticker or FIGI required") 1156 1157 else: 1158 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1159 1160 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1161 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1162 self.body = str({"figi": self.figi, "depth": self.depth}) 1163 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1164 1165 if pricesResponse and not ("code" in pricesResponse.keys() or "message" in pricesResponse.keys() or "description" in pricesResponse.keys()): 1166 # list of dicts with sellers orders: 1167 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1168 1169 # list of dicts with buyers orders: 1170 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1171 1172 # max price of instrument at this time: 1173 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1174 1175 # min price of instrument at this time: 1176 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1177 1178 # last price of deal with instrument: 1179 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1180 1181 # last close price of instrument: 1182 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1183 1184 else: 1185 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1186 uLogger.debug("Server response: {}".format(pricesResponse)) 1187 1188 if show: 1189 if prices["buy"] or prices["sell"]: 1190 info = [ 1191 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1192 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1193 self.ticker, 1194 self.figi, 1195 self.depth, 1196 ), 1197 "-" * 60, "\n", 1198 " Orders of Buyers | Orders of Sellers\n", 1199 "-" * 60, "\n", 1200 " Sell prices (volumes) | Buy prices (volumes)\n", 1201 "-" * 60, "\n", 1202 ] 1203 1204 if not prices["buy"]: 1205 info.append(" | No orders!\n") 1206 sumBuy = 0 1207 1208 else: 1209 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1210 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1211 for item in maxMinSorted: 1212 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1213 1214 if not prices["sell"]: 1215 info.append("No orders! |\n") 1216 sumSell = 0 1217 1218 else: 1219 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1220 for item in prices["sell"]: 1221 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1222 1223 info.extend([ 1224 "-" * 60, "\n", 1225 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1226 "-" * 60, "\n", 1227 ]) 1228 1229 infoText = "".join(info) 1230 1231 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1232 1233 else: 1234 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1235 1236 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1238 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1239 """ 1240 This method get and show information about all available broker instruments for current user account. 1241 If `instrumentsFile` string is not empty then also save information to this file. 1242 1243 :param show: if `True` then print results to console, if `False` — print only to file. 1244 :return: multi-lines string with all available broker instruments 1245 """ 1246 if not self.iList: 1247 self.iList = self.Listing() 1248 1249 info = [ 1250 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1251 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1252 ] 1253 1254 # add instruments count by type: 1255 for iType in self.iList.keys(): 1256 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1257 1258 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1259 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1260 1261 # generating info tables with all instruments by type: 1262 for iType in self.iList.keys(): 1263 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1264 1265 for instrument in self.iList[iType].keys(): 1266 iName = self.iList[iType][instrument]["name"] # instrument's name 1267 if len(iName) > 57: 1268 iName = "{}...".format(iName[:54]) # right trim for a long string 1269 1270 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1271 self.iList[iType][instrument]["ticker"], 1272 iName, 1273 self.iList[iType][instrument]["figi"], 1274 self.iList[iType][instrument]["currency"], 1275 self.iList[iType][instrument]["lot"], 1276 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1277 )) 1278 1279 infoText = "".join(info) 1280 1281 if show: 1282 uLogger.info(infoText) 1283 1284 if self.instrumentsFile: 1285 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1286 fH.write(infoText) 1287 1288 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1289 1290 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse— print only to file.
Returns
multi-lines string with all available broker instruments
1292 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` — return list of result only. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile: 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse— return list of result only.
Returns
list of dictionaries with all found instruments.
1371 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1372 """ 1373 Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs. 1374 1375 :param instruments: list of strings with tickers or FIGIs. 1376 :return: list with unique instrument FIGIs only. 1377 """ 1378 requestedInstruments = [] 1379 for iName in instruments: 1380 if iName not in self.aliases.keys(): 1381 if iName not in requestedInstruments: 1382 requestedInstruments.append(iName) 1383 1384 else: 1385 if iName not in requestedInstruments: 1386 if self.aliases[iName] not in requestedInstruments: 1387 requestedInstruments.append(self.aliases[iName]) 1388 1389 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1390 1391 onlyUniqueFIGIs = [] 1392 for iName in requestedInstruments: 1393 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1394 continue 1395 1396 self.ticker = iName 1397 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1398 1399 if not iData: 1400 self.ticker = "" 1401 self.figi = iName 1402 1403 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1404 1405 if not iData: 1406 self.figi = "" 1407 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1408 1409 if iData and iData["figi"] not in onlyUniqueFIGIs: 1410 onlyUniqueFIGIs.append(iData["figi"]) 1411 1412 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1413 1414 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers (priority) or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1416 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1417 """ 1418 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1419 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 1422 If `pricesFile` string is not empty then also save information to this file. 1423 1424 :param instruments: list of strings with tickers or FIGIs. 1425 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1426 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1427 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1428 """ 1429 if instruments is None or not instruments: 1430 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1431 raise Exception("Ticker or FIGI required") 1432 1433 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1434 1435 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1436 1437 iList = [] # trying to get info and current prices about all unique instruments: 1438 for self.figi in onlyUniqueFIGIs: 1439 iData = self.SearchByFIGI(requestPrice=True) 1440 iList.append(iData) 1441 1442 self.ShowListOfPrices(iList, show) 1443 1444 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1446 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1447 """ 1448 Show table contains current prices of given instruments. 1449 1450 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1451 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1452 :param show: if `True` then prints prices to console, if `False` — prints only to file `pricesFile`. 1453 :return: multilines text in Markdown format as a table contains current prices. 1454 """ 1455 infoText = "" 1456 1457 if show or self.pricesFile: 1458 info = [ 1459 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1460 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1461 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1462 ] 1463 1464 for item in iList: 1465 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1466 item["ticker"], 1467 item["figi"], 1468 item["type"], 1469 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1470 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1471 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1472 "{} / {}".format( 1473 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1474 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1475 ), 1476 "{} / {}".format( 1477 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1478 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1479 ), 1480 item["currency"], 1481 )) 1482 1483 infoText = "".join(info) 1484 1485 if show: 1486 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1487 1488 if self.pricesFile: 1489 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1490 fH.write(infoText) 1491 1492 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1493 1494 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse— prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1496 def RequestTradingStatus(self) -> dict: 1497 """ 1498 Requesting trading status for the instrument defined by `figi` variable. 1499 1500 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1501 1502 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1503 1504 :return: dictionary with trading status attributes. Response example: 1505 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1506 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1507 """ 1508 if self.figi is None or not self.figi: 1509 uLogger.error("Variable `figi` must be defined for using this method!") 1510 raise Exception("FIGI required") 1511 1512 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1513 1514 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1515 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1516 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1517 1518 if self.moreDebug: 1519 uLogger.debug("Records about current trading status successfully received") 1520 1521 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1523 def RequestPortfolio(self) -> dict: 1524 """ 1525 Requesting actual user's portfolio for current `accountId`. 1526 1527 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1528 1529 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1530 1531 :return: dictionary with user's portfolio. 1532 """ 1533 if self.accountId is None or not self.accountId: 1534 uLogger.error("Variable `accountId` must be defined for using this method!") 1535 raise Exception("Account ID required") 1536 1537 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1538 1539 self.body = str({"accountId": self.accountId}) 1540 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1541 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1542 1543 if self.moreDebug: 1544 uLogger.debug("Records about user's portfolio successfully received") 1545 1546 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1548 def RequestPositions(self) -> dict: 1549 """ 1550 Requesting open positions by currencies and instruments for current `accountId`. 1551 1552 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1553 1554 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1555 1556 :return: dictionary with open positions by instruments. 1557 """ 1558 if self.accountId is None or not self.accountId: 1559 uLogger.error("Variable `accountId` must be defined for using this method!") 1560 raise Exception("Account ID required") 1561 1562 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1563 1564 self.body = str({"accountId": self.accountId}) 1565 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1566 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1567 1568 if self.moreDebug: 1569 uLogger.debug("Records about current open positions successfully received") 1570 1571 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1573 def RequestPendingOrders(self) -> list: 1574 """ 1575 Requesting current actual pending orders for current `accountId`. 1576 1577 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1578 1579 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1580 1581 :return: list of dictionaries with pending orders. 1582 """ 1583 if self.accountId is None or not self.accountId: 1584 uLogger.error("Variable `accountId` must be defined for using this method!") 1585 raise Exception("Account ID required") 1586 1587 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1588 1589 self.body = str({"accountId": self.accountId}) 1590 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1591 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1592 1593 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1594 1595 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1597 def RequestStopOrders(self) -> list: 1598 """ 1599 Requesting current actual stop orders for current `accountId`. 1600 1601 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1602 1603 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1604 1605 :return: list of dictionaries with stop orders. 1606 """ 1607 if self.accountId is None or not self.accountId: 1608 uLogger.error("Variable `accountId` must be defined for using this method!") 1609 raise Exception("Account ID required") 1610 1611 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1612 1613 self.body = str({"accountId": self.accountId}) 1614 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1615 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1616 1617 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1618 1619 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1621 def Overview(self, show: bool = False, details: str = "full") -> dict: 1622 """ 1623 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1624 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1625 and `overviewBondsCalendarFile` are defined then also save information to file. 1626 1627 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1628 many requests about the state of the portfolio, and then, based on the received data, a large number 1629 of calculation and statistics are collected. 1630 1631 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1632 :param details: how detailed should the information be? 1633 - `full` — shows full available information about portfolio status (by default), 1634 - `positions` — shows only open positions, 1635 - `orders` — shows only sections of open limits and stop orders. 1636 - `digest` — show a short digest of the portfolio status, 1637 - `analytics` — shows only the analytics section and the distribution of the portfolio by various categories, 1638 - `calendar` — shows only the bonds calendar section (if these present in portfolio), 1639 :return: dictionary with client's raw portfolio and some statistics. 1640 """ 1641 if self.accountId is None or not self.accountId: 1642 uLogger.error("Variable `accountId` must be defined for using this method!") 1643 raise Exception("Account ID required") 1644 1645 view = { 1646 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1647 "headers": {}, # list of dictionaries, response headers without "positions" section 1648 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1649 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1650 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1651 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1652 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1653 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1654 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1655 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1656 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1657 }, 1658 "stat": { # --- some statistics calculated using "raw" sections: 1659 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1660 "availableRUB": 0., # available rubles (without other currencies) 1661 "blockedRUB": 0., # blocked sum in Russian Rouble 1662 "totalChangesRUB": 0., # changes for all open trades in RUB 1663 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1664 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1665 "sharesCostRUB": 0., # costs of all shares in RUB 1666 "bondsCostRUB": 0., # costs of all bonds in RUB 1667 "etfsCostRUB": 0., # costs of all etfs in RUB 1668 "futuresCostRUB": 0., # costs of all futures in RUB 1669 "Currencies": [], # list of dictionaries of all currencies statistics 1670 "Shares": [], # list of dictionaries of all shares statistics 1671 "Bonds": [], # list of dictionaries of all bonds statistics 1672 "Etfs": [], # list of dictionaries of all etfs statistics 1673 "Futures": [], # list of dictionaries of all futures statistics 1674 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1675 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1676 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1677 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1678 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1679 }, 1680 "analytics": { # --- some analytics of portfolio: 1681 "distrByAssets": {}, # portfolio distribution by assets 1682 "distrByCompanies": {}, # portfolio distribution by companies 1683 "distrBySectors": {}, # portfolio distribution by sectors 1684 "distrByCurrencies": {}, # portfolio distribution by currencies 1685 "distrByCountries": {}, # portfolio distribution by countries 1686 "bondsCalendar": None, # bonds payment calendar as Pandas DataFrame (if these present in portfolio) 1687 } 1688 } 1689 1690 details = details.lower() 1691 availableDetails = ["full", "positions", "orders", "analytics", "calendar", "digest"] 1692 if details not in availableDetails: 1693 details = "full" 1694 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1695 1696 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1697 1698 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1699 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1700 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1701 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1702 1703 # save response headers without "positions" section: 1704 for key in portfolioResponse.keys(): 1705 if key != "positions": 1706 view["raw"]["headers"][key] = portfolioResponse[key] 1707 1708 else: 1709 continue 1710 1711 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1712 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1713 for item in portfolioResponse["positions"]: 1714 if item["instrumentType"] == "currency": 1715 self.figi = item["figi"] 1716 curr = self.SearchByFIGI(requestPrice=False) 1717 1718 # current price of currency in RUB: 1719 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1720 "name": curr["name"], 1721 "currentPrice": NanoToFloat( 1722 item["currentPrice"]["units"], 1723 item["currentPrice"]["nano"] 1724 ), 1725 } 1726 1727 view["raw"]["Currencies"].append(item) 1728 1729 elif item["instrumentType"] == "share": 1730 view["raw"]["Shares"].append(item) 1731 1732 elif item["instrumentType"] == "bond": 1733 view["raw"]["Bonds"].append(item) 1734 1735 elif item["instrumentType"] == "etf": 1736 view["raw"]["Etfs"].append(item) 1737 1738 elif item["instrumentType"] == "futures": 1739 view["raw"]["Futures"].append(item) 1740 1741 else: 1742 continue 1743 1744 # how many volume of currencies (by ISO currency name) are blocked: 1745 for item in view["raw"]["positions"]["blocked"]: 1746 blocked = NanoToFloat(item["units"], item["nano"]) 1747 if blocked > 0: 1748 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1749 1750 # how many volume of instruments (by FIGI) are blocked: 1751 for item in view["raw"]["positions"]["securities"]: 1752 blocked = int(item["blocked"]) 1753 if blocked > 0: 1754 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1755 1756 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1757 1758 if "rub" in allBlocked.keys(): 1759 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1760 1761 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1762 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1763 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1764 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1765 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1766 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1767 view["stat"]["portfolioCostRUB"] = sum([ 1768 view["stat"]["allCurrenciesCostRUB"], 1769 view["stat"]["sharesCostRUB"], 1770 view["stat"]["bondsCostRUB"], 1771 view["stat"]["etfsCostRUB"], 1772 view["stat"]["futuresCostRUB"], 1773 ]) 1774 1775 # --- calculating some portfolio statistics: 1776 byComp = {} # distribution by companies 1777 bySect = {} # distribution by sectors 1778 byCurr = {} # distribution by currencies (include RUB) 1779 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1780 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1781 1782 for item in portfolioResponse["positions"]: 1783 self.figi = item["figi"] 1784 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1785 1786 if instrument: 1787 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1788 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1789 1790 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1791 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1792 1793 else: 1794 blocked = 0 1795 1796 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1797 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1798 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1799 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1800 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1801 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1802 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1803 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1804 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1805 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1806 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1807 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1808 1809 statData = { 1810 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1811 "ticker": instrument["ticker"], # ticker by FIGI 1812 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1813 "volume": volume, # available volume of instrument 1814 "lots": lots, # volume in lots of instrument 1815 "direction": direction, # direction of an instrument's position: short or long 1816 "blocked": blocked, # blocked volume of currency or instrument 1817 "currentPrice": curPrice, # current instrument's price in basic asset 1818 "average": average, # current average position price 1819 "cost": cost, # current cost of all volume of instrument in basic asset 1820 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1821 "costRUB": costRUB, # cost of instrument in ruble 1822 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1823 "profit": profit, # expected profit at current moment 1824 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1825 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1826 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1827 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1828 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1829 "step": instrument["step"], # minimum price increment 1830 } 1831 1832 # adding distribution by unique countries: 1833 if statData["country"] not in byCountry.keys(): 1834 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1835 1836 else: 1837 byCountry[statData["country"]]["cost"] += costRUB 1838 byCountry[statData["country"]]["percent"] += percentCostRUB 1839 1840 if item["instrumentType"] != "currency": 1841 # adding distribution by unique companies: 1842 if statData["name"]: 1843 if statData["name"] not in byComp.keys(): 1844 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1845 1846 else: 1847 byComp[statData["name"]]["cost"] += costRUB 1848 byComp[statData["name"]]["percent"] += percentCostRUB 1849 1850 # adding distribution by unique sectors: 1851 if statData["sector"] not in bySect.keys(): 1852 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1853 1854 else: 1855 bySect[statData["sector"]]["cost"] += costRUB 1856 bySect[statData["sector"]]["percent"] += percentCostRUB 1857 1858 # adding distribution by unique currencies: 1859 if currency not in byCurr.keys(): 1860 byCurr[currency] = { 1861 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1862 "cost": costRUB, 1863 "percent": percentCostRUB 1864 } 1865 1866 else: 1867 byCurr[currency]["cost"] += costRUB 1868 byCurr[currency]["percent"] += percentCostRUB 1869 1870 # saving statistics for every instrument: 1871 if item["instrumentType"] == "currency": 1872 view["stat"]["Currencies"].append(statData) 1873 1874 # update dict with free funds for trading (total - blocked) by currencies 1875 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1876 view["stat"]["funds"][currency] = { 1877 "total": volume, 1878 "totalCostRUB": costRUB, # total volume cost in rubles 1879 "free": volume - blocked, 1880 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1881 } 1882 1883 elif item["instrumentType"] == "share": 1884 view["stat"]["Shares"].append(statData) 1885 1886 elif item["instrumentType"] == "bond": 1887 view["stat"]["Bonds"].append(statData) 1888 1889 elif item["instrumentType"] == "etf": 1890 view["stat"]["Etfs"].append(statData) 1891 1892 elif item["instrumentType"] == "Futures": 1893 view["stat"]["Futures"].append(statData) 1894 1895 else: 1896 continue 1897 1898 # total changes in Russian Ruble: 1899 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1900 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1901 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1902 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1903 view["stat"]["funds"]["rub"] = { 1904 "total": view["stat"]["availableRUB"], 1905 "totalCostRUB": view["stat"]["availableRUB"], 1906 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1907 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1908 } 1909 1910 # --- pending orders sector data: 1911 uniquePendingOrdersFIGIs = [] # unique FIGIs of pending orders to avoid many times price requests 1912 uniquePendingOrders = {} # unique instruments with FIGIs as dictionary keys 1913 1914 for item in view["raw"]["orders"]: 1915 self.figi = item["figi"] 1916 1917 if item["figi"] not in uniquePendingOrdersFIGIs: 1918 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1919 1920 uniquePendingOrdersFIGIs.append(item["figi"]) 1921 uniquePendingOrders[item["figi"]] = instrument 1922 1923 else: 1924 instrument = uniquePendingOrders[item["figi"]] 1925 1926 if instrument: 1927 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1928 orderType = TKS_ORDER_TYPES[item["orderType"]] 1929 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1930 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1931 1932 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1933 if item["direction"] == "ORDER_DIRECTION_BUY": 1934 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1935 1936 else: 1937 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1938 1939 # requested price for order execution: 1940 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1941 1942 # necessary changes in percent to reach target from current price: 1943 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1944 1945 view["stat"]["orders"].append({ 1946 "orderID": item["orderId"], # orderId number parameter of current order 1947 "figi": item["figi"], # FIGI identification 1948 "ticker": instrument["ticker"], # ticker name by FIGI 1949 "lotsRequested": item["lotsRequested"], # requested lots value 1950 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1951 "currentPrice": lastPrice, # current instrument's price for defined action 1952 "targetPrice": target, # requested price for order execution in base currency 1953 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1954 "percentChanges": changes, # changes in percent to target from current price 1955 "currency": item["currency"], # instrument's currency name 1956 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1957 "type": orderType, # type of order from TKS_ORDER_TYPES 1958 "status": orderState, # order status from TKS_ORDER_STATES 1959 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1960 }) 1961 1962 # --- stop orders sector data: 1963 uniqueStopOrdersFIGIs = [] # unique FIGIs of stop orders to avoid many times price requests 1964 uniqueStopOrders = {} # unique instruments with FIGIs as dictionary keys 1965 1966 for item in view["raw"]["stopOrders"]: 1967 self.figi = item["figi"] 1968 1969 if item["figi"] not in uniqueStopOrdersFIGIs: 1970 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI, price requests only one time 1971 1972 uniqueStopOrdersFIGIs.append(item["figi"]) 1973 uniqueStopOrders[item["figi"]] = instrument 1974 1975 else: 1976 instrument = uniqueStopOrders[item["figi"]] 1977 1978 if instrument: 1979 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1980 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1981 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1982 1983 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1984 if "expirationTime" in item.keys(): 1985 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1986 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1987 1988 else: 1989 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1990 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1991 1992 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1993 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1994 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1995 1996 else: 1997 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1998 1999 # requested price when stop-order executed: 2000 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 2001 2002 # price for limit-order, set up when stop-order executed: 2003 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 2004 2005 # necessary changes in percent to reach target from current price: 2006 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 2007 2008 view["stat"]["stopOrders"].append({ 2009 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 2010 "figi": item["figi"], # FIGI identification 2011 "ticker": instrument["ticker"], # ticker name by FIGI 2012 "lotsRequested": item["lotsRequested"], # requested lots value 2013 "currentPrice": lastPrice, # current instrument's price for defined action 2014 "targetPrice": target, # requested price for stop-order execution in base currency 2015 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 2016 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 2017 "percentChanges": changes, # changes in percent to target from current price 2018 "currency": item["currency"], # instrument's currency name 2019 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 2020 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 2021 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 2022 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 2023 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2024 }) 2025 2026 # --- calculating data for analytics section: 2027 # portfolio distribution by assets: 2028 view["analytics"]["distrByAssets"] = { 2029 "Ruble": { 2030 "uniques": 1, 2031 "cost": view["stat"]["availableRUB"], 2032 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 "Currencies": { 2035 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2036 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2037 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2038 }, 2039 "Shares": { 2040 "uniques": len(view["stat"]["Shares"]), 2041 "cost": view["stat"]["sharesCostRUB"], 2042 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2043 }, 2044 "Bonds": { 2045 "uniques": len(view["stat"]["Bonds"]), 2046 "cost": view["stat"]["bondsCostRUB"], 2047 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2048 }, 2049 "Etfs": { 2050 "uniques": len(view["stat"]["Etfs"]), 2051 "cost": view["stat"]["etfsCostRUB"], 2052 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2053 }, 2054 "Futures": { 2055 "uniques": len(view["stat"]["Futures"]), 2056 "cost": view["stat"]["futuresCostRUB"], 2057 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2058 }, 2059 } 2060 2061 # portfolio distribution by companies: 2062 view["analytics"]["distrByCompanies"]["All money cash"] = { 2063 "ticker": "", 2064 "cost": view["stat"]["allCurrenciesCostRUB"], 2065 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2066 } 2067 view["analytics"]["distrByCompanies"].update(byComp) 2068 2069 # portfolio distribution by sectors: 2070 view["analytics"]["distrBySectors"]["All money cash"] = { 2071 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2072 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2073 } 2074 view["analytics"]["distrBySectors"].update(bySect) 2075 2076 # portfolio distribution by currencies: 2077 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2078 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2079 2080 if self.moreDebug: 2081 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2082 2083 view["analytics"]["distrByCurrencies"].update(byCurr) 2084 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2085 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2086 2087 # portfolio distribution by countries: 2088 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2089 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2090 2091 if self.moreDebug: 2092 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2093 2094 view["analytics"]["distrByCountries"].update(byCountry) 2095 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2096 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2097 2098 # --- Prepare text statistics overview in human-readable: 2099 if show: 2100 # Whatever the value `details`, header not changes: 2101 info = [ 2102 "# Client's portfolio\n\n", 2103 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2104 "* **Account ID:** [{}]\n".format(self.accountId), 2105 ] 2106 2107 if details in ["full", "positions", "digest"]: 2108 info.extend([ 2109 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2110 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2111 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2112 view["stat"]["totalChangesRUB"], 2113 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2114 view["stat"]["totalChangesPercentRUB"], 2115 ), 2116 ]) 2117 2118 if details in ["full", "positions"]: 2119 info.extend([ 2120 "## Open positions\n\n", 2121 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2122 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2123 "| Ruble | {:>31} | | | | | |\n".format( 2124 "{:.2f} ({:.2f}) rub".format( 2125 view["stat"]["availableRUB"], 2126 view["stat"]["blockedRUB"], 2127 ) 2128 ) 2129 ]) 2130 2131 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2132 return [ 2133 "| | | | | | | |\n", 2134 "| {:<27} | | | | | {:>19} | |\n".format( 2135 noTradeStr if noTradeStr else typeStr, 2136 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2137 ), 2138 ] 2139 2140 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2141 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2142 "{} [{}]".format(data["ticker"], data["figi"]), 2143 "{:.2f} ({:.2f}) {}".format( 2144 data["volume"], 2145 data["blocked"], 2146 data["currency"], 2147 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2148 data["volume"], 2149 data["blocked"], 2150 ), 2151 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2152 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2153 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2154 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2155 "{}{:.2f} {} ({}{:.2f}%)".format( 2156 "+" if data["profit"] > 0 else "", 2157 data["profit"], data["baseCurrencyName"], 2158 "+" if data["percentProfit"] > 0 else "", 2159 data["percentProfit"], 2160 ), 2161 ) 2162 2163 # --- Show currencies section: 2164 if view["stat"]["Currencies"]: 2165 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2166 for item in view["stat"]["Currencies"]: 2167 info.append(_InfoStr(item, showCurrencyName=True)) 2168 2169 else: 2170 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2171 2172 # --- Show shares section: 2173 if view["stat"]["Shares"]: 2174 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2175 2176 for item in view["stat"]["Shares"]: 2177 info.append(_InfoStr(item)) 2178 2179 else: 2180 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2181 2182 # --- Show bonds section: 2183 if view["stat"]["Bonds"]: 2184 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2185 2186 for item in view["stat"]["Bonds"]: 2187 info.append(_InfoStr(item)) 2188 2189 else: 2190 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2191 2192 # --- Show etfs section: 2193 if view["stat"]["Etfs"]: 2194 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2195 2196 for item in view["stat"]["Etfs"]: 2197 info.append(_InfoStr(item)) 2198 2199 else: 2200 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2201 2202 # --- Show futures section: 2203 if view["stat"]["Futures"]: 2204 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2205 2206 for item in view["stat"]["Futures"]: 2207 info.append(_InfoStr(item)) 2208 2209 else: 2210 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2211 2212 if details in ["full", "orders"]: 2213 # --- Show pending orders section: 2214 if view["stat"]["orders"]: 2215 info.extend([ 2216 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2217 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2218 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["orders"]: 2222 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 item["action"], 2234 item["type"], 2235 item["date"], 2236 )) 2237 2238 else: 2239 info.append("\n## Total pending limit-orders: 0\n") 2240 2241 # --- Show stop orders section: 2242 if view["stat"]["stopOrders"]: 2243 info.extend([ 2244 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2245 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2246 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2247 ]) 2248 2249 for item in view["stat"]["stopOrders"]: 2250 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2251 "{} [{}]".format(item["ticker"], item["figi"]), 2252 item["orderID"], 2253 item["lotsRequested"], 2254 "{} {} ({}{:.2f}%)".format( 2255 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2256 item["baseCurrencyName"], 2257 "+" if item["percentChanges"] > 0 else "", 2258 float(item["percentChanges"]), 2259 ), 2260 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2261 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2262 item["action"], 2263 item["type"], 2264 item["expType"], 2265 item["createDate"], 2266 item["expDate"], 2267 )) 2268 2269 else: 2270 info.append("\n## Total stop-orders: 0\n") 2271 2272 if details in ["full", "analytics"]: 2273 # -- Show analytics section: 2274 if view["stat"]["portfolioCostRUB"] > 0: 2275 info.extend([ 2276 "\n# Analytics\n" 2277 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2278 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2279 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2280 view["stat"]["totalChangesRUB"], 2281 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2282 view["stat"]["totalChangesPercentRUB"], 2283 ), 2284 "\n## Portfolio distribution by assets\n" 2285 "\n| Type | Uniques | Percent | Current cost |\n", 2286 "|------------------------------------|---------|---------|--------------------|\n", 2287 ]) 2288 2289 for key in view["analytics"]["distrByAssets"].keys(): 2290 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2291 info.append("| {:<34} | {:<7} | {:<7} | {:<18} |\n".format( 2292 key, 2293 view["analytics"]["distrByAssets"][key]["uniques"], 2294 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2295 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2296 )) 2297 2298 aSepLine = "|----------------------------------------------|---------|--------------------|\n" 2299 2300 info.extend([ 2301 "\n## Portfolio distribution by companies\n" 2302 "\n| Company | Percent | Current cost |\n", 2303 aSepLine, 2304 ]) 2305 2306 for company in view["analytics"]["distrByCompanies"].keys(): 2307 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2308 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2309 "{}{}".format( 2310 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2311 company, 2312 ), 2313 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2314 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2315 )) 2316 2317 info.extend([ 2318 "\n## Portfolio distribution by sectors\n" 2319 "\n| Sector | Percent | Current cost |\n", 2320 aSepLine, 2321 ]) 2322 2323 for sector in view["analytics"]["distrBySectors"].keys(): 2324 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2325 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2326 sector, 2327 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2328 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2329 )) 2330 2331 info.extend([ 2332 "\n## Portfolio distribution by currencies\n" 2333 "\n| Instruments currencies | Percent | Current cost |\n", 2334 aSepLine, 2335 ]) 2336 2337 for curr in view["analytics"]["distrByCurrencies"].keys(): 2338 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2339 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2340 "[{}] {}".format(curr, view["analytics"]["distrByCurrencies"][curr]["name"]), 2341 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2343 )) 2344 2345 info.extend([ 2346 "\n## Portfolio distribution by countries\n" 2347 "\n| Assets by country | Percent | Current cost |\n", 2348 aSepLine, 2349 ]) 2350 2351 for country in view["analytics"]["distrByCountries"].keys(): 2352 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2353 info.append("| {:<44} | {:<7} | {:<18} |\n".format( 2354 country, 2355 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2356 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2357 )) 2358 2359 if details in ["full", "calendar"]: 2360 # -- Show bonds payment calendar section: 2361 if view["stat"]["Bonds"]: 2362 bondTickers = [item["ticker"] for item in view["stat"]["Bonds"]] 2363 view["analytics"]["bondsCalendar"] = self.ExtendBondsData(instruments=bondTickers, xlsx=False) 2364 info.append("\n" + self.ShowBondsCalendar(extBonds=view["analytics"]["bondsCalendar"], show=False)) 2365 2366 else: 2367 info.append("\n# Bond payments calendar\n\nNo bonds in the portfolio to create payments calendar\n") 2368 2369 infoText = "".join(info) 2370 2371 uLogger.info(infoText) 2372 2373 if details == "full" and self.overviewFile: 2374 filename = self.overviewFile 2375 2376 elif details == "digest" and self.overviewDigestFile: 2377 filename = self.overviewDigestFile 2378 2379 elif details == "positions" and self.overviewPositionsFile: 2380 filename = self.overviewPositionsFile 2381 2382 elif details == "orders" and self.overviewOrdersFile: 2383 filename = self.overviewOrdersFile 2384 2385 elif details == "analytics" and self.overviewAnalyticsFile: 2386 filename = self.overviewAnalyticsFile 2387 2388 elif details == "calendar" and self.overviewBondsCalendarFile: 2389 filename = self.overviewBondsCalendarFile 2390 2391 else: 2392 filename = "" 2393 2394 if filename: 2395 with open(filename, "w", encoding="UTF-8") as fH: 2396 fH.write(infoText) 2397 2398 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2399 2400 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
and overviewBondsCalendarFile are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be?
full— shows full available information about portfolio status (by default),positions— shows only open positions,orders— shows only sections of open limits and stop orders.digest— show a short digest of the portfolio status,analytics— shows only the analytics section and the distribution of the portfolio by various categories,calendar— shows only the bonds calendar section (if these present in portfolio),
Returns
dictionary with client's raw portfolio and some statistics.
2402 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple[list[dict], dict]: 2403 """ 2404 Returns history operations between two given dates for current `accountId`. 2405 If `reportFile` string is not empty then also save human-readable report. 2406 Shows some statistical data of closed positions. 2407 2408 :param start: see docstring in `GetDatesAsString()` method 2409 :param end: see docstring in `GetDatesAsString()` method 2410 :param show: if `True` then also prints all records to the console. 2411 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2412 :return: original list of dictionaries with history of deals records from API ("operations" key): 2413 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2414 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2415 """ 2416 if self.accountId is None or not self.accountId: 2417 uLogger.error("Variable `accountId` must be defined for using this method!") 2418 raise Exception("Account ID required") 2419 2420 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2421 2422 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2423 2424 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2425 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2426 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2427 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2428 customStat = {} # custom statistics in additional to responseJSON 2429 2430 # --- output report in human-readable format: 2431 if show or self.reportFile: 2432 splitLine1 = "| | | | | |\n" # Summary section 2433 splitLine2 = "| | | | | | | | |\n" # Operations section 2434 nextDay = "" 2435 2436 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2437 2438 if len(ops) > 0: 2439 customStat = { 2440 "opsCount": 0, # total operations count 2441 "buyCount": 0, # buy operations 2442 "sellCount": 0, # sell operations 2443 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2444 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2445 "payIn": {"rub": 0.}, # Deposit brokerage account 2446 "payOut": {"rub": 0.}, # Withdrawals 2447 "divs": {"rub": 0.}, # Dividends income 2448 "coupons": {"rub": 0.}, # Coupon's income 2449 "brokerCom": {"rub": 0.}, # Service commissions 2450 "serviceCom": {"rub": 0.}, # Service commissions 2451 "marginCom": {"rub": 0.}, # Margin commissions 2452 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2453 } 2454 2455 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2456 for item in ops: 2457 if item["state"] == "OPERATION_STATE_EXECUTED": 2458 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2459 2460 # count buy operations: 2461 if "_BUY" in item["operationType"]: 2462 customStat["buyCount"] += 1 2463 2464 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2465 customStat["buyTotal"][item["payment"]["currency"]] += payment 2466 2467 else: 2468 customStat["buyTotal"][item["payment"]["currency"]] = payment 2469 2470 # count sell operations: 2471 elif "_SELL" in item["operationType"]: 2472 customStat["sellCount"] += 1 2473 2474 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2475 customStat["sellTotal"][item["payment"]["currency"]] += payment 2476 2477 else: 2478 customStat["sellTotal"][item["payment"]["currency"]] = payment 2479 2480 # count incoming operations: 2481 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2482 if item["payment"]["currency"] in customStat["payIn"].keys(): 2483 customStat["payIn"][item["payment"]["currency"]] += payment 2484 2485 else: 2486 customStat["payIn"][item["payment"]["currency"]] = payment 2487 2488 # count withdrawals operations: 2489 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2490 if item["payment"]["currency"] in customStat["payOut"].keys(): 2491 customStat["payOut"][item["payment"]["currency"]] += payment 2492 2493 else: 2494 customStat["payOut"][item["payment"]["currency"]] = payment 2495 2496 # count dividends income: 2497 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2498 if item["payment"]["currency"] in customStat["divs"].keys(): 2499 customStat["divs"][item["payment"]["currency"]] += payment 2500 2501 else: 2502 customStat["divs"][item["payment"]["currency"]] = payment 2503 2504 # count coupon's income: 2505 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2506 if item["payment"]["currency"] in customStat["coupons"].keys(): 2507 customStat["coupons"][item["payment"]["currency"]] += payment 2508 2509 else: 2510 customStat["coupons"][item["payment"]["currency"]] = payment 2511 2512 # count broker commissions: 2513 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2514 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2515 customStat["brokerCom"][item["payment"]["currency"]] += payment 2516 2517 else: 2518 customStat["brokerCom"][item["payment"]["currency"]] = payment 2519 2520 # count service commissions: 2521 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2522 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2523 customStat["serviceCom"][item["payment"]["currency"]] += payment 2524 2525 else: 2526 customStat["serviceCom"][item["payment"]["currency"]] = payment 2527 2528 # count margin commissions: 2529 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2530 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2531 customStat["marginCom"][item["payment"]["currency"]] += payment 2532 2533 else: 2534 customStat["marginCom"][item["payment"]["currency"]] = payment 2535 2536 # count withholding taxes: 2537 elif "_TAX" in item["operationType"]: 2538 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2539 customStat["allTaxes"][item["payment"]["currency"]] += payment 2540 2541 else: 2542 customStat["allTaxes"][item["payment"]["currency"]] = payment 2543 2544 else: 2545 continue 2546 2547 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2548 2549 # --- view "Actions" lines: 2550 info.extend([ 2551 "| Report sections | | | | |\n", 2552 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2553 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2554 "| | Buy: {:<22} | {:<28} | | |\n".format( 2555 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2556 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2557 ), 2558 "| | Sell: {:<21} | {:<28} | | |\n".format( 2559 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2560 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2561 ), 2562 ]) 2563 2564 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2565 for key in opsKeys: 2566 if key == "rub": 2567 continue 2568 2569 info.extend([ 2570 "| | | {:<28} | | |\n".format( 2571 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2572 ), 2573 "| | | {:<28} | | |\n".format( 2574 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2575 ), 2576 ]) 2577 2578 info.append(splitLine1) 2579 2580 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2581 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2582 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2583 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2584 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2585 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2586 ) 2587 2588 # --- view "Payments" lines: 2589 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2590 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2591 2592 for key in paymentsKeys: 2593 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2594 2595 info.append(splitLine1) 2596 2597 # --- view "Commissions and taxes" lines: 2598 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2599 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2600 2601 for key in comKeys: 2602 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2603 2604 info.append(splitLine1) 2605 2606 info.extend([ 2607 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2608 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2609 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2610 ]) 2611 2612 else: 2613 info.append("Broker returned no operations during this period\n") 2614 2615 # --- view "Operations" section: 2616 for item in ops: 2617 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2618 continue 2619 2620 else: 2621 self.figi = item["figi"] if item["figi"] else "" 2622 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2623 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2624 2625 # group of deals during one day: 2626 if nextDay and item["date"].split("T")[0] != nextDay: 2627 info.append(splitLine2) 2628 nextDay = "" 2629 2630 else: 2631 nextDay = item["date"].split("T")[0] # saving current day for splitting 2632 2633 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2634 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2635 self.figi if self.figi else "—", 2636 instrument["ticker"] if instrument else "—", 2637 instrument["type"] if instrument else "—", 2638 item["quantity"] if int(item["quantity"]) > 0 else "—", 2639 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2640 TKS_OPERATION_STATES[item["state"]], 2641 TKS_OPERATION_TYPES[item["operationType"]], 2642 )) 2643 2644 infoText = "".join(info) 2645 2646 if show: 2647 if self.moreDebug: 2648 uLogger.debug("Records about history of a client's operations successfully received") 2649 2650 uLogger.info(infoText) 2651 2652 if self.reportFile: 2653 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2654 fH.write(infoText) 2655 2656 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2657 2658 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2660 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2661 """ 2662 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2663 2664 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2665 Warning! Broker server used ISO UTC time by default. 2666 2667 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2668 Also, `historyFile` used to update history with `onlyMissing` parameter. 2669 2670 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2671 2672 :param start: see docstring in `GetDatesAsString()` method. 2673 :param end: see docstring in `GetDatesAsString()` method. 2674 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2675 `"hour"`, `"day"`. Default: `"hour"`. 2676 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2677 False by default. Warning! History appends only from last candle to current time 2678 with always update last candle! 2679 :param csvSep: separator if csv-file is used, `,` by default. 2680 :param show: if `True` then also prints Pandas DataFrame to the console. 2681 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2682 `["date", "time", "open", "high", "low", "close", "volume"]`. 2683 """ 2684 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2685 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2686 history = None # empty pandas object for history 2687 2688 if interval not in TKS_CANDLE_INTERVALS.keys(): 2689 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2690 raise Exception("Incorrect value") 2691 2692 if not (self.ticker or self.figi): 2693 uLogger.error("Ticker or FIGI must be defined!") 2694 raise Exception("Ticker or FIGI required") 2695 2696 if self.ticker and not self.figi: 2697 instrumentByTicker = self.SearchByTicker(requestPrice=False) 2698 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2699 2700 if self.figi and not self.ticker: 2701 instrumentByFIGI = self.SearchByFIGI(requestPrice=False) 2702 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2703 2704 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2705 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2706 if interval.lower() != "day": 2707 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2708 2709 delta = dtEnd - dtStart # current UTC time minus last time in file 2710 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2711 2712 # calculate history length in candles: 2713 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2714 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2715 length += 1 # to avoid fraction time 2716 2717 # calculate data blocks count: 2718 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2719 2720 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2721 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2722 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2723 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2724 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2725 2726 tempOld = None # pandas object for old history, if --only-missing key present 2727 lastTime = None # datetime object of last old candle in file 2728 2729 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2730 uLogger.debug("--only-missing key present, add only last missing candles...") 2731 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2732 2733 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2734 2735 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2736 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2737 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2738 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2739 2740 # get last datetime object from last string in file or minus 1 delta if file is empty: 2741 if len(tempOld) > 0: 2742 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2743 2744 else: 2745 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2746 2747 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2748 2749 responseJSONs = [] # raw history blocks of data 2750 2751 blockEnd = dtEnd 2752 for item in range(blocks): 2753 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2754 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2755 2756 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2757 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2758 )) 2759 2760 if blockStart == blockEnd: 2761 uLogger.debug("Skipped this zero-length block...") 2762 2763 else: 2764 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2765 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2766 self.body = str({ 2767 "figi": self.figi, 2768 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2769 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2770 "interval": TKS_CANDLE_INTERVALS[interval][0] 2771 }) 2772 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1) 2773 2774 if "code" in responseJSON.keys(): 2775 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2776 2777 else: 2778 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2779 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2780 2781 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2782 2783 blockEnd = blockStart 2784 2785 printCount = len(responseJSONs) # candles to show in console 2786 if responseJSONs: 2787 tempHistory = pd.DataFrame( 2788 data={ 2789 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2790 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2791 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2792 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2793 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2794 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2795 "volume": [int(item["volume"]) for item in responseJSONs], 2796 }, 2797 index=range(len(responseJSONs)), 2798 columns=["date", "time", "open", "high", "low", "close", "volume"], 2799 ) 2800 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2801 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2802 2803 # append only newest candles to old history if --only-missing key present: 2804 if onlyMissing and tempOld is not None and lastTime is not None: 2805 index = 0 # find start index in tempHistory data: 2806 2807 for i, item in tempHistory.iterrows(): 2808 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2809 2810 if curTime == lastTime: 2811 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2812 index = i 2813 printCount = index + 1 2814 break 2815 2816 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2817 2818 else: 2819 history = tempHistory # if no `--only-missing` key then load full data from server 2820 2821 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2822 2823 if history is not None and not history.empty: 2824 if show: 2825 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2826 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2827 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2828 )) 2829 2830 else: 2831 uLogger.warning("Received an empty candles history!") 2832 2833 if self.historyFile is not None: 2834 if history is not None and not history.empty: 2835 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2836 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2837 2838 else: 2839 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2840 2841 else: 2842 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2843 2844 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2846 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2847 """ 2848 Load candles history from csv-file and return Pandas DataFrame object. 2849 2850 See also: `History()` and `ShowHistoryChart()` methods. 2851 2852 :param filePath: path to csv-file to open. 2853 """ 2854 loadedHistory = None # init candles data object 2855 2856 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2857 2858 if os.path.exists(filePath): 2859 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2860 2861 tfStr = self.priceModel.FormattedDelta( 2862 self.priceModel.timeframe, 2863 "{days} days {hours}h {minutes}m {seconds}s", 2864 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2865 self.priceModel.timeframe, 2866 "{hours}h {minutes}m {seconds}s", 2867 ) 2868 2869 if loadedHistory is not None and not loadedHistory.empty: 2870 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2871 len(loadedHistory), 2872 tfStr, 2873 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2874 ) 2875 2876 else: 2877 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2878 2879 else: 2880 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2881 2882 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2884 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2885 """ 2886 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2887 2888 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2889 Default: `index.html` (both for interact and non-interact candlesticks chart). 2890 2891 See also: `History()` and `LoadHistory()` methods. 2892 2893 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2894 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2895 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2896 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2897 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2898 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2899 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2900 """ 2901 if isinstance(candles, str): 2902 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2903 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2904 2905 elif isinstance(candles, pd.DataFrame): 2906 self.priceModel.prices = candles # set candles chain from variable 2907 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2908 2909 if "datetime" not in candles.columns: 2910 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2911 2912 else: 2913 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2914 raise Exception("Incorrect value") 2915 2916 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2917 2918 if interact: 2919 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2920 2921 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2922 2923 else: 2924 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2925 2926 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2927 2928 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2930 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2931 """ 2932 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2933 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2934 2935 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2936 2937 :param operation: string "Buy" or "Sell". 2938 :param lots: volume, integer count of lots >= 1. 2939 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2940 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2941 :param expDate: string "Undefined" by default or local date in future, 2942 it is a string with format `%Y-%m-%d %H:%M:%S`. 2943 :return: JSON with response from broker server. 2944 """ 2945 if self.accountId is None or not self.accountId: 2946 uLogger.error("Variable `accountId` must be defined for using this method!") 2947 raise Exception("Account ID required") 2948 2949 if operation is None or not operation or operation not in ("Buy", "Sell"): 2950 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2951 raise Exception("Incorrect value") 2952 2953 if lots is None or lots < 1: 2954 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2955 lots = 1 2956 2957 if tp is None or tp < 0: 2958 tp = 0 2959 2960 if sl is None or sl < 0: 2961 sl = 0 2962 2963 if expDate is None or not expDate: 2964 expDate = "Undefined" 2965 2966 if not (self.ticker or self.figi): 2967 uLogger.error("Ticker or FIGI must be defined!") 2968 raise Exception("Ticker or FIGI required") 2969 2970 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 2971 self.ticker = instrument["ticker"] 2972 self.figi = instrument["figi"] 2973 2974 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2975 2976 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2977 self.body = str({ 2978 "figi": self.figi, 2979 "quantity": str(lots), 2980 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2981 "accountId": str(self.accountId), 2982 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2983 }) 2984 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0) 2985 2986 if "orderId" in response.keys(): 2987 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2988 operation, response["orderId"], 2989 self.ticker, self.figi, lots, 2990 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2991 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2992 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2993 )) 2994 2995 if tp > 0: 2996 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2997 2998 if sl > 0: 2999 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 3000 3001 else: 3002 uLogger.warning("Not `oK` status received! Market order not executed. See full debug log or try again and open order later.") 3003 3004 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3006 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3007 """ 3008 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 3009 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 3010 3011 See also: `Order()` and `Trade()` docstrings. 3012 3013 :param lots: volume, integer count of lots >= 1. 3014 :param tp: float > 0, take profit price of stop-order. 3015 :param sl: float > 0, stop loss price of stop-order. 3016 :param expDate: it's a local date in future. 3017 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3018 :return: JSON with response from broker server. 3019 """ 3020 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3022 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 3023 """ 3024 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 3025 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 3026 3027 See also: `Order()` and `Trade()` docstrings. 3028 3029 :param lots: volume, integer count of lots >= 1. 3030 :param tp: float > 0, take profit price of stop-order. 3031 :param sl: float > 0, stop loss price of stop-order. 3032 :param expDate: it's a local date in the future. 3033 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3034 :return: JSON with response from broker server. 3035 """ 3036 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3038 def CloseTrades(self, instruments: list[str], portfolio: dict = None) -> None: 3039 """ 3040 Close position of given instruments. 3041 3042 :param instruments: list of instruments defined by tickers or FIGIs that must be closed. 3043 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3044 This avoids unnecessary downloading data from the server. 3045 """ 3046 if instruments is None or not instruments: 3047 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3048 raise Exception("Ticker or FIGI required") 3049 3050 if isinstance(instruments, str): 3051 instruments = [instruments] 3052 3053 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3054 if uniqueInstruments: 3055 if portfolio is None or not portfolio: 3056 portfolio = self.Overview(show=False) 3057 3058 allOpened = [item["figi"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3059 uLogger.debug("All opened instruments by it's FIGI: {}".format(", ".join(allOpened))) 3060 3061 for self.figi in uniqueInstruments: 3062 if self.figi not in allOpened: 3063 uLogger.warning("Instrument with FIGI [{}] not in open positions list!".format(self.figi)) 3064 continue 3065 3066 # search open trade info about instrument by ticker: 3067 instrument = {} 3068 for iType in TKS_INSTRUMENTS: 3069 if instrument: 3070 break 3071 3072 for item in portfolio["stat"][iType]: 3073 if item["figi"] == self.figi: 3074 instrument = item 3075 break 3076 3077 if instrument: 3078 self.ticker = instrument["ticker"] 3079 self.figi = instrument["figi"] 3080 3081 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3082 self.ticker, 3083 self.figi, 3084 int(instrument["volume"]), 3085 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3086 )) 3087 3088 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3089 3090 if tradeLots > 0: 3091 if instrument["blocked"] > 0: 3092 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3093 instrument["blocked"], 3094 self.ticker, 3095 tradeLots, 3096 )) 3097 3098 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3099 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3100 3101 else: 3102 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- instruments: list of instruments defined by tickers or FIGIs that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3104 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3105 """ 3106 Close all positions of given instruments with defined type. 3107 3108 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3109 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3110 This avoids unnecessary downloading data from the server. 3111 """ 3112 if iType not in TKS_INSTRUMENTS: 3113 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3114 3115 else: 3116 if portfolio is None or not portfolio: 3117 portfolio = self.Overview(show=False) 3118 3119 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3120 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3121 3122 if tickers and portfolio: 3123 self.CloseTrades(tickers, portfolio) 3124 3125 else: 3126 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3128 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3129 """ 3130 Universal method to create market or limit orders with all available parameters for current `accountId`. 3131 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3132 3133 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3134 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3135 3136 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3137 then broker immediately open market order as you can do simple --buy or --sell operations! 3138 3139 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3140 When current price will go up or down to target price value then broker opens a limit order. 3141 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3142 3143 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3144 3145 :param operation: string "Buy" or "Sell". 3146 :param orderType: string "Limit" or "Stop". 3147 :param lots: volume, integer count of lots >= 1. 3148 :param targetPrice: target price > 0. This is open trade price for limit order. 3149 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3150 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3151 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3152 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3153 Stop loss order always executed by market price. 3154 :param expDate: string "Undefined" by default or local date in future. 3155 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3156 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3157 A limit order has no expiration date, it lasts until the end of the trading day. 3158 :return: JSON with response from broker server. 3159 """ 3160 if self.accountId is None or not self.accountId: 3161 uLogger.error("Variable `accountId` must be defined for using this method!") 3162 raise Exception("Account ID required") 3163 3164 if operation is None or not operation or operation not in ("Buy", "Sell"): 3165 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3166 raise Exception("Incorrect value") 3167 3168 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3169 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3170 raise Exception("Incorrect value") 3171 3172 if lots is None or lots < 1: 3173 uLogger.error("You must define trade volume > 0: integer count of lots!") 3174 raise Exception("Incorrect value") 3175 3176 if targetPrice is None or targetPrice <= 0: 3177 uLogger.error("Target price for limit-order must be greater than 0!") 3178 raise Exception("Incorrect value") 3179 3180 if limitPrice is None or limitPrice <= 0: 3181 limitPrice = targetPrice 3182 3183 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3184 stopType = "Limit" 3185 3186 if expDate is None or not expDate: 3187 expDate = "Undefined" 3188 3189 if not (self.ticker or self.figi): 3190 uLogger.error("Tocker or FIGI must be defined!") 3191 raise Exception("Ticker or FIGI required") 3192 3193 response = {} 3194 instrument = self.SearchByTicker(requestPrice=True) if self.ticker else self.SearchByFIGI(requestPrice=True) 3195 self.ticker = instrument["ticker"] 3196 self.figi = instrument["figi"] 3197 3198 if orderType == "Limit": 3199 uLogger.debug( 3200 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3201 self.ticker, self.figi, 3202 operation, lots, targetPrice, instrument["currency"], 3203 )) 3204 3205 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3206 self.body = str({ 3207 "figi": self.figi, 3208 "quantity": str(lots), 3209 "price": FloatToNano(targetPrice), 3210 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3211 "accountId": str(self.accountId), 3212 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3213 }) 3214 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3215 3216 if "orderId" in response.keys(): 3217 uLogger.info( 3218 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3219 response["orderId"], 3220 self.ticker, self.figi, 3221 operation, lots, targetPrice, instrument["currency"], 3222 )) 3223 3224 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3225 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3226 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3227 targetPrice, instrument["currency"], 3228 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3229 )) 3230 3231 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3232 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3233 targetPrice, instrument["currency"], 3234 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3235 )) 3236 3237 else: 3238 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3239 3240 if orderType == "Stop": 3241 uLogger.debug( 3242 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3243 self.ticker, self.figi, 3244 operation, lots, 3245 targetPrice, instrument["currency"], 3246 limitPrice, instrument["currency"], 3247 stopType, expDate, 3248 )) 3249 3250 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3251 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3252 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3253 3254 body = { 3255 "figi": self.figi, 3256 "quantity": str(lots), 3257 "price": FloatToNano(limitPrice), 3258 "stopPrice": FloatToNano(targetPrice), 3259 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3260 "accountId": str(self.accountId), 3261 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3262 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3263 } 3264 3265 if expDateUTC: 3266 body["expireDate"] = expDateUTC 3267 3268 self.body = str(body) 3269 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0) 3270 3271 if "stopOrderId" in response.keys(): 3272 uLogger.info( 3273 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3274 response["stopOrderId"], 3275 self.ticker, self.figi, 3276 operation, lots, 3277 targetPrice, instrument["currency"], 3278 limitPrice, instrument["currency"], 3279 TKS_STOP_ORDER_TYPES[stopOrderType], 3280 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3281 )) 3282 3283 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3284 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3285 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3286 targetPrice, instrument["currency"], 3287 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3288 )) 3289 3290 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3291 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3292 targetPrice, instrument["currency"], 3293 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3294 )) 3295 3296 else: 3297 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3298 3299 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3301 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3302 """ 3303 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3304 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3305 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3306 See also: `Order()` docstring. 3307 3308 :param lots: volume, integer count of lots >= 1. 3309 :param targetPrice: target price > 0. This is open trade price for limit order. 3310 :return: JSON with response from broker server. 3311 """ 3312 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3314 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3315 """ 3316 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3317 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3318 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3319 target price value then broker opens a limit order. See also: `Order()` docstring. 3320 3321 :param lots: volume, integer count of lots >= 1. 3322 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3323 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3324 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3325 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3326 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3327 :param expDate: string "Undefined" by default or local date in future. 3328 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3329 This date is converting to UTC format for server. 3330 :return: JSON with response from broker server. 3331 """ 3332 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3334 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3335 """ 3336 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3337 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3338 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3339 See also: `Order()` docstring. 3340 3341 :param lots: volume, integer count of lots >= 1. 3342 :param targetPrice: target price > 0. This is open trade price for limit order. 3343 :return: JSON with response from broker server. 3344 """ 3345 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3347 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3348 """ 3349 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3350 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3351 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3352 target price value then broker opens a limit order. See also: `Order()` docstring. 3353 3354 :param lots: volume, integer count of lots >= 1. 3355 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3356 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3357 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3358 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3359 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3360 :param expDate: string "Undefined" by default or local date in future. 3361 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3362 This date is converting to UTC format for server. 3363 :return: JSON with response from broker server. 3364 """ 3365 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3367 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3368 """ 3369 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3370 3371 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3372 :param allOrdersIDs: pre-received lists of all active pending orders. 3373 This avoids unnecessary downloading data from the server. 3374 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3375 """ 3376 if self.accountId is None or not self.accountId: 3377 uLogger.error("Variable `accountId` must be defined for using this method!") 3378 raise Exception("Account ID required") 3379 3380 if orderIDs: 3381 if allOrdersIDs is None or not allOrdersIDs: 3382 rawOrders = self.RequestPendingOrders() 3383 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3384 3385 if allStopOrdersIDs is None or not allStopOrdersIDs: 3386 rawStopOrders = self.RequestStopOrders() 3387 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3388 3389 for orderID in orderIDs: 3390 idInPendingOrders = orderID in allOrdersIDs 3391 idInStopOrders = orderID in allStopOrdersIDs 3392 3393 if not (idInPendingOrders or idInStopOrders): 3394 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3395 continue 3396 3397 else: 3398 if idInPendingOrders: 3399 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3400 3401 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3402 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3403 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3404 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3405 3406 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3407 if self.moreDebug: 3408 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3409 3410 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3411 3412 else: 3413 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3414 3415 elif idInStopOrders: 3416 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3417 3418 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3419 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3420 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3421 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3422 3423 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3424 if self.moreDebug: 3425 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3426 3427 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3428 3429 else: 3430 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3431 3432 else: 3433 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3435 def CloseAllOrders(self) -> None: 3436 """ 3437 Gets a list of open pending and stop orders and cancel it all. 3438 """ 3439 rawOrders = self.RequestPendingOrders() 3440 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3441 lenOrders = len(allOrdersIDs) 3442 3443 rawStopOrders = self.RequestStopOrders() 3444 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3445 lenSOrders = len(allStopOrdersIDs) 3446 3447 if lenOrders > 0 or lenSOrders > 0: 3448 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3449 3450 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3451 3452 else: 3453 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3455 def CloseAll(self, *args) -> None: 3456 """ 3457 Close all available (not blocked) opened trades and orders. 3458 3459 Also, you can select one or more keywords case-insensitive: 3460 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3461 3462 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3463 """ 3464 overview = self.Overview(show=False) # get all open trades info 3465 3466 if len(args) == 0: 3467 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3468 self.CloseAllOrders() # close all pending and stop orders 3469 3470 for iType in TKS_INSTRUMENTS: 3471 if iType != "Currencies": 3472 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3473 3474 else: 3475 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3476 lowerArgs = [x.lower() for x in args] 3477 3478 if "orders" in lowerArgs: 3479 self.CloseAllOrders() # close all pending and stop orders 3480 3481 for iType in TKS_INSTRUMENTS: 3482 if iType.lower() in lowerArgs and iType != "Currencies": 3483 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3485 @staticmethod 3486 def ParseOrderParameters(operation, **inputParameters): 3487 """ 3488 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3489 3490 :param operation: string "Buy" or "Sell". 3491 :param inputParameters: this is dict of strings that looks like this 3492 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3493 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3494 "prices" key: one or more prices to open limit-orders 3495 Counts of values in lots and prices lists must be equals! 3496 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3497 """ 3498 # TODO: update order grid work with api v2 3499 pass 3500 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3501 # 3502 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3503 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3504 # raise Exception("Incorrect value") 3505 # 3506 # if "l" in inputParameters.keys(): 3507 # inputParameters["lots"] = inputParameters.pop("l") 3508 # 3509 # if "p" in inputParameters.keys(): 3510 # inputParameters["prices"] = inputParameters.pop("p") 3511 # 3512 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3513 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3514 # raise Exception("Incorrect value") 3515 # 3516 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3517 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3518 # 3519 # if len(lots) != len(prices): 3520 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3521 # raise Exception("Incorrect value") 3522 # 3523 # uLogger.debug("Extracted parameters for orders:") 3524 # uLogger.debug("lots = {}".format(lots)) 3525 # uLogger.debug("prices = {}".format(prices)) 3526 # 3527 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3528 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3529 # uLogger.debug("Order parameters: {}".format(result)) 3530 # 3531 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3533 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3534 """ 3535 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3536 3537 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3538 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3539 """ 3540 result = False 3541 msg = "Instrument not defined!" 3542 3543 if portfolio is None or not portfolio: 3544 portfolio = self.Overview(show=False) 3545 3546 if self.ticker: 3547 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3548 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3549 3550 for iType in TKS_INSTRUMENTS: 3551 for instrument in portfolio["stat"][iType]: 3552 if instrument["ticker"] == self.ticker: 3553 result = True 3554 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3555 break 3556 3557 elif self.figi: 3558 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3559 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3560 3561 for iType in TKS_INSTRUMENTS: 3562 for instrument in portfolio["stat"][iType]: 3563 if instrument["figi"] == self.figi: 3564 result = True 3565 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3566 break 3567 3568 else: 3569 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3570 3571 uLogger.debug(msg) 3572 3573 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3575 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3576 """ 3577 Returns instrument is in the user's portfolio if it presents there. 3578 Instrument must be defined by `ticker` (highly priority) or `figi`. 3579 3580 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3581 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3582 """ 3583 result = None 3584 msg = "Instrument not defined!" 3585 3586 if portfolio is None or not portfolio: 3587 portfolio = self.Overview(show=False) 3588 3589 if self.ticker: 3590 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3591 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3592 3593 for iType in TKS_INSTRUMENTS: 3594 for instrument in portfolio["stat"][iType]: 3595 if instrument["ticker"] == self.ticker: 3596 result = instrument 3597 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3598 break 3599 3600 elif self.figi: 3601 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3602 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3603 3604 for iType in TKS_INSTRUMENTS: 3605 for instrument in portfolio["stat"][iType]: 3606 if instrument["figi"] == self.figi: 3607 result = instrument 3608 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3609 break 3610 3611 else: 3612 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3613 3614 uLogger.debug(msg) 3615 3616 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3618 def RequestLimits(self) -> dict: 3619 """ 3620 Method for obtaining the available funds for withdrawal for current `accountId`. 3621 3622 See also: 3623 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3624 - `OverviewLimits()` method 3625 3626 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3627 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3628 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3629 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3630 """ 3631 if self.accountId is None or not self.accountId: 3632 uLogger.error("Variable `accountId` must be defined for using this method!") 3633 raise Exception("Account ID required") 3634 3635 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3636 3637 self.body = str({"accountId": self.accountId}) 3638 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3639 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3640 3641 if self.moreDebug: 3642 uLogger.debug("Records about available funds for withdrawal successfully received") 3643 3644 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3646 def OverviewLimits(self, show: bool = False) -> dict: 3647 """ 3648 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3649 3650 See also: `RequestLimits()`. 3651 3652 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3653 :return: dict with raw parsed data from server and some calculated statistics about it. 3654 """ 3655 if self.accountId is None or not self.accountId: 3656 uLogger.error("Variable `accountId` must be defined for using this method!") 3657 raise Exception("Account ID required") 3658 3659 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3660 3661 view = { 3662 "rawLimits": rawLimits, 3663 "limits": { # parsed data for every currency: 3664 "money": { # this is an array of portfolio currency positions 3665 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3666 }, 3667 "blocked": { # this is an array of blocked currency 3668 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3669 }, 3670 "blockedGuarantee": { # this is locked money under collateral for futures 3671 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3672 }, 3673 }, 3674 } 3675 3676 # --- Prepare text table with limits in human-readable format: 3677 if show: 3678 info = [ 3679 "# Withdrawal limits\n\n", 3680 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3681 "* **Account ID:** [{}]\n".format(self.accountId), 3682 ] 3683 3684 if view["limits"]["money"]: 3685 info.extend([ 3686 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3687 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3688 ]) 3689 3690 else: 3691 info.append("\nNo withdrawal limits\n") 3692 3693 for curr in view["limits"]["money"].keys(): 3694 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3695 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3696 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3697 3698 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3699 "[{}]".format(curr), 3700 "{:.2f}".format(view["limits"]["money"][curr]), 3701 "{:.2f}".format(availableMoney), 3702 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3703 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3704 ) 3705 3706 if curr == "rub": 3707 info.insert(5, infoStr) # hack: insert "rub" at the first position in table and after headers 3708 3709 else: 3710 info.append(infoStr) 3711 3712 infoText = "".join(info) 3713 3714 uLogger.info(infoText) 3715 3716 if self.withdrawalLimitsFile: 3717 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3718 fH.write(infoText) 3719 3720 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3721 3722 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3724 def RequestAccounts(self) -> dict: 3725 """ 3726 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3727 3728 See also: 3729 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3730 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3731 - `OverviewUserInfo()` method 3732 3733 :return: dict with raw data from server that contains accounts info. Example of dict: 3734 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3735 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3736 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3737 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3738 """ 3739 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3740 3741 self.body = str({}) 3742 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3743 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3744 3745 if self.moreDebug: 3746 uLogger.debug("Records about available accounts successfully received") 3747 3748 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3750 def RequestUserInfo(self) -> dict: 3751 """ 3752 Method for requesting common user's information. 3753 3754 See also: 3755 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3756 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3757 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3758 - `OverviewUserInfo()` method 3759 3760 :return: dict with raw data from server that contains user's information. Example of dict: 3761 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3762 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3763 """ 3764 uLogger.debug("Requesting common user's information. Wait, please...") 3765 3766 self.body = str({}) 3767 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3768 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3769 3770 if self.moreDebug: 3771 uLogger.debug("Records about current user successfully received") 3772 3773 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3775 def RequestMarginStatus(self, accountId: str = None) -> dict: 3776 """ 3777 Method for requesting margin calculation for defined account ID. 3778 3779 See also: 3780 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3781 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3782 - `OverviewUserInfo()` method 3783 3784 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3785 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3786 Example of responses: 3787 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3788 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3789 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3790 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3791 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3792 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3793 """ 3794 if accountId is None or not accountId: 3795 if self.accountId is None or not self.accountId: 3796 uLogger.error("Variable `accountId` must be defined for using this method!") 3797 raise Exception("Account ID required") 3798 3799 else: 3800 accountId = self.accountId # use `self.accountId` (main ID) by default 3801 3802 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3803 3804 self.body = str({"accountId": accountId}) 3805 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3806 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3807 3808 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3809 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3810 rawMargin = {} 3811 3812 else: 3813 if self.moreDebug: 3814 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3815 3816 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3818 def RequestTariffLimits(self) -> dict: 3819 """ 3820 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3821 3822 See also: 3823 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3824 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3825 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3826 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3827 - `OverviewUserInfo()` method 3828 3829 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3830 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3831 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3832 """ 3833 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3834 3835 self.body = str({}) 3836 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3837 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3838 3839 if self.moreDebug: 3840 uLogger.debug("Records with limits of current tariff successfully received") 3841 3842 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3844 def RequestBondCoupons(self, iJSON: dict) -> dict: 3845 """ 3846 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3847 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3848 All dates are in UTC timezone. 3849 3850 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3851 Documentation: 3852 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3853 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3854 3855 See also: `ExtendBondsData()`. 3856 3857 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3858 If raw iJSON is not data of bond then server returns an error [400] with message: 3859 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3860 :return: dictionary with bond payment calendar. Response example 3861 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3862 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3863 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3864 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3865 """ 3866 if iJSON["figi"] is None or not iJSON["figi"]: 3867 uLogger.error("FIGI must be defined for using this method!") 3868 raise Exception("FIGI required") 3869 3870 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3871 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3872 3873 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3874 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3875 self.figi, 3876 startDate, 3877 endDate, 3878 )) 3879 3880 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3881 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3882 calendar = self.SendAPIRequest(calendarURL, reqType="POST") 3883 3884 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3885 uLogger.warning("Instrument type is not bond!") 3886 3887 else: 3888 if self.moreDebug: 3889 uLogger.debug("Records about bond payment calendar successfully received") 3890 3891 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3893 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3894 """ 3895 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3896 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3897 coupon yields, current yields and some statistics etc. 3898 3899 WARNING! This is too long operation if a lot of bonds requested from broker server. 3900 3901 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3902 3903 :param instruments: list of strings with tickers or FIGIs. 3904 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3905 for further used by data scientists or stock analytics. 3906 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3907 In XLSX-file and Pandas DataFrame fields mean: 3908 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3909 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3910 """ 3911 if instruments is None or not instruments: 3912 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3913 raise Exception("Ticker or FIGI required") 3914 3915 if isinstance(instruments, str): 3916 instruments = [instruments] 3917 3918 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3919 3920 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3921 3922 iCount = len(uniqueInstruments) 3923 tooLong = iCount >= 20 3924 if tooLong: 3925 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3926 3927 bonds = None 3928 for i, self.figi in enumerate(uniqueInstruments): 3929 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3930 3931 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3932 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3933 rawBond = self.SearchByFIGI(requestPrice=True) 3934 3935 # Widen raw data with UTC current time (iData["actualDateTime"]): 3936 actualDate = datetime.now(tzutc()) 3937 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3938 3939 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3940 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3941 3942 # Replace some values with human-readable: 3943 iData["nominalCurrency"] = iData["nominal"]["currency"] 3944 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3945 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3946 iData["aciCurrency"] = iData["aciValue"]["currency"] 3947 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3948 iData["issueSize"] = int(iData["issueSize"]) 3949 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3950 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3951 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3952 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3953 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3954 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3955 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3956 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3957 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3958 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3959 3960 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3961 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3962 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3963 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3964 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3965 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3966 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3967 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3968 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3969 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3970 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3971 3972 # Widen raw data with calendar data from `rawCalendar` values: 3973 calendarData = [] 3974 if "events" in iData["rawCalendar"].keys(): 3975 for item in iData["rawCalendar"]["events"]: 3976 calendarData.append({ 3977 "couponDate": item["couponDate"], 3978 "couponNumber": int(item["couponNumber"]), 3979 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3980 "payCurrency": item["payOneBond"]["currency"], 3981 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3982 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3983 "couponStartDate": item["couponStartDate"], 3984 "couponEndDate": item["couponEndDate"], 3985 "couponPeriod": item["couponPeriod"], 3986 }) 3987 3988 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3989 if "maturityDate" not in iData.keys(): 3990 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3991 3992 # Widen raw data with Coupon Rate. 3993 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3994 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3995 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3996 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3997 3998 # Widen raw data with Yield to Maturity (YTM) on current date. 3999 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 4000 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 4001 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 4002 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 4003 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 4004 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 4005 4006 iData["calendar"] = calendarData # adds calendar at the end 4007 4008 # Remove not used data: 4009 iData.pop("uid") 4010 iData.pop("positionUid") 4011 iData.pop("currentPrice") 4012 iData.pop("rawCalendar") 4013 4014 colNames = list(iData.keys()) 4015 if bonds is None: 4016 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 4017 4018 else: 4019 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 4020 4021 else: 4022 uLogger.warning("Instrument is not a bond!") 4023 4024 processed = round(100 * (i + 1) / iCount, 1) 4025 if tooLong and processed % 5 == 0: 4026 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 4027 4028 else: 4029 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 4030 4031 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 4032 4033 # Saving bonds from Pandas DataFrame to XLSX sheet: 4034 if xlsx and self.bondsXLSXFile: 4035 with pd.ExcelWriter( 4036 path=self.bondsXLSXFile, 4037 date_format=TKS_DATE_FORMAT, 4038 datetime_format=TKS_DATE_TIME_FORMAT, 4039 mode="w", 4040 ) as writer: 4041 bonds.to_excel( 4042 writer, 4043 sheet_name="Extended bonds data", 4044 index=True, 4045 encoding="UTF-8", 4046 freeze_panes=(1, 1), 4047 ) # saving as XLSX-file with freeze first row and column as headers 4048 4049 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 4050 4051 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4053 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4054 """ 4055 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4056 4057 WARNING! This is too long operation if a lot of bonds requested from broker server. 4058 4059 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4060 4061 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4062 extended information about bonds: main info, current prices, bond payment calendar, 4063 coupon yields, current yields and some statistics etc. 4064 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4065 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4066 for further used by data scientists or stock analytics. 4067 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4068 """ 4069 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4070 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4071 4072 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4073 4074 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4075 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4076 calendar = None 4077 for bond in extBonds.iterrows(): 4078 for item in bond[1]["calendar"]: 4079 cData = { 4080 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4081 "couponDate": item["couponDate"], 4082 "figi": bond[1]["figi"], 4083 "ticker": bond[1]["ticker"], 4084 "name": bond[1]["name"], 4085 "couponNumber": item["couponNumber"], 4086 "payOneBond": item["payOneBond"], 4087 "payCurrency": item["payCurrency"], 4088 "couponType": item["couponType"], 4089 "couponPeriod": item["couponPeriod"], 4090 "fixDate": item["fixDate"], 4091 "couponStartDate": item["couponStartDate"], 4092 "couponEndDate": item["couponEndDate"], 4093 } 4094 4095 if calendar is None: 4096 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4097 4098 else: 4099 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4100 4101 if calendar is not None: 4102 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4103 4104 # Saving calendar from Pandas DataFrame to XLSX sheet: 4105 if xlsx: 4106 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4107 4108 with pd.ExcelWriter( 4109 path=xlsxCalendarFile, 4110 date_format=TKS_DATE_FORMAT, 4111 datetime_format=TKS_DATE_TIME_FORMAT, 4112 mode="w", 4113 ) as writer: 4114 humanReadable = calendar.copy(deep=True) 4115 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4116 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4117 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4118 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4119 humanReadable.columns = colNames # human-readable column names 4120 4121 humanReadable.to_excel( 4122 writer, 4123 sheet_name="Bond payments calendar", 4124 index=False, 4125 encoding="UTF-8", 4126 freeze_panes=(1, 2), 4127 ) # saving as XLSX-file with freeze first row and column as headers 4128 4129 del humanReadable # release df in memory 4130 4131 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4132 4133 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4135 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4136 """ 4137 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4138 Also, creates Markdown file with calendar data, `calendar.md` by default. 4139 4140 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4141 4142 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4143 extended information about bonds: main info, current prices, bond payment calendar, 4144 coupon yields, current yields and some statistics etc. 4145 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4146 :param show: if `True` then also printing bonds payment calendar to the console, 4147 otherwise save to file `calendarFile` only. `False` by default. 4148 :return: multilines text in Markdown format with bonds payment calendar as a table. 4149 """ 4150 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4151 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4152 4153 infoText = "# Bond payments calendar\n\n" 4154 4155 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4156 4157 if not (calendar is None or calendar.empty): 4158 splitLine = "| | | | | | | | | |\n" 4159 4160 info = [ 4161 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4162 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4163 ] 4164 4165 newMonth = False 4166 notOneBond = calendar["figi"].nunique() > 1 4167 for i, bond in enumerate(calendar.iterrows()): 4168 if newMonth and notOneBond: 4169 info.append(splitLine) 4170 4171 info.append( 4172 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4173 " √" if bond[1]["paid"] else " —", 4174 bond[1]["couponDate"].split("T")[0], 4175 bond[1]["figi"], 4176 bond[1]["ticker"], 4177 bond[1]["couponNumber"], 4178 "{} {}".format( 4179 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4180 bond[1]["payCurrency"], 4181 ), 4182 bond[1]["couponType"], 4183 bond[1]["couponPeriod"], 4184 bond[1]["fixDate"].split("T")[0], 4185 ) 4186 ) 4187 4188 if i < len(calendar.values) - 1: 4189 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4190 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4191 newMonth = False if curDate.month == nextDate.month else True 4192 4193 else: 4194 newMonth = False 4195 4196 infoText += "".join(info) 4197 4198 if show: 4199 uLogger.info("{}".format(infoText)) 4200 4201 if self.calendarFile is not None: 4202 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4203 fH.write(infoText) 4204 4205 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4206 4207 else: 4208 infoText += "No data\n" 4209 4210 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4212 def OverviewAccounts(self, show: bool = False) -> dict: 4213 """ 4214 Method for parsing and show simple table with all available user accounts. 4215 4216 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4217 4218 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4219 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4220 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4221 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4222 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4223 "closed": "—", "access": "Full access" }, ...}}` 4224 """ 4225 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4226 4227 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4228 accounts = { 4229 item["id"]: { 4230 "type": TKS_ACCOUNT_TYPES[item["type"]], 4231 "name": item["name"], 4232 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4233 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4234 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4235 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4236 } for item in rawAccounts["accounts"] 4237 } 4238 4239 # Raw and parsed data with some fields replaced in "stat" section: 4240 view = { 4241 "rawAccounts": rawAccounts, 4242 "stat": accounts, 4243 } 4244 4245 # --- Prepare simple text table with only accounts data in human-readable format: 4246 if show: 4247 info = [ 4248 "# User accounts\n\n", 4249 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4250 "| Account ID | Type | Status | Name |\n", 4251 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4252 ] 4253 4254 for account in view["stat"].keys(): 4255 info.extend([ 4256 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4257 account, 4258 view["stat"][account]["type"], 4259 view["stat"][account]["status"], 4260 view["stat"][account]["name"], 4261 ) 4262 ]) 4263 4264 infoText = "".join(info) 4265 4266 uLogger.info(infoText) 4267 4268 if self.userAccountsFile: 4269 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4270 fH.write(infoText) 4271 4272 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4273 4274 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4276 def OverviewUserInfo(self, show: bool = False) -> dict: 4277 """ 4278 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4279 4280 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4281 4282 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4283 :return: dict with raw parsed data from server and some calculated statistics about it. 4284 """ 4285 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4286 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4287 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4288 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4289 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4290 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4291 4292 # This is dict with parsed common user data: 4293 userInfo = { 4294 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4295 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4296 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4297 "tariff": rawUserInfo["tariff"], 4298 } 4299 4300 # This is an array of dict with parsed margin statuses for every account IDs: 4301 margins = {} 4302 for accountId in accounts.keys(): 4303 if rawMargins[accountId]: 4304 margins[accountId] = { 4305 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4306 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4307 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4308 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4309 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4310 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4311 } 4312 4313 else: 4314 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4315 4316 unary = {} # unary-connection limits 4317 for item in rawTariffLimits["unaryLimits"]: 4318 if item["limitPerMinute"] in unary.keys(): 4319 unary[item["limitPerMinute"]].extend(item["methods"]) 4320 4321 else: 4322 unary[item["limitPerMinute"]] = item["methods"] 4323 4324 stream = {} # stream-connection limits 4325 for item in rawTariffLimits["streamLimits"]: 4326 if item["limit"] in stream.keys(): 4327 stream[item["limit"]].extend(item["streams"]) 4328 4329 else: 4330 stream[item["limit"]] = item["streams"] 4331 4332 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4333 limits = { 4334 "unary": unary, 4335 "stream": stream, 4336 } 4337 4338 # Raw and parsed data as an output result: 4339 view = { 4340 "rawUserInfo": rawUserInfo, 4341 "rawAccounts": rawAccounts, 4342 "rawMargins": rawMargins, 4343 "rawTariffLimits": rawTariffLimits, 4344 "stat": { 4345 "userInfo": userInfo, 4346 "accounts": accounts, 4347 "margins": margins, 4348 "limits": limits, 4349 }, 4350 } 4351 4352 # --- Prepare text table with user information in human-readable format: 4353 if show: 4354 info = [ 4355 "# Full user information\n\n", 4356 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4357 "## Common information\n\n", 4358 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4359 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4360 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4361 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4362 "\n## User accounts\n\n", 4363 ] 4364 4365 for account in view["stat"]["accounts"].keys(): 4366 info.extend([ 4367 "### ID: [{}]\n\n".format(account), 4368 "| Parameters | Values |\n", 4369 "|----------------------|--------------------------------------------------------------|\n", 4370 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4371 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4372 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4373 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4374 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4375 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4376 ]) 4377 4378 if margins[account]: 4379 info.extend([ 4380 "| Margin status: | Enabled |\n", 4381 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4382 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4383 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4384 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4385 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4386 ]) 4387 4388 else: 4389 info.append("| Margin status: | Disabled |\n\n") 4390 4391 info.extend([ 4392 "\n## Current user tariff limits\n", 4393 "\nSee also:\n", 4394 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4395 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4396 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4397 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4398 "\n### Unary limits\n", 4399 ]) 4400 4401 if unary: 4402 for key, values in sorted(unary.items()): 4403 info.append("\n* Max requests per minute: {}\n".format(key)) 4404 4405 for value in values: 4406 info.append(" - {}\n".format(value)) 4407 4408 else: 4409 info.append("\nNot available\n") 4410 4411 info.append("\n### Stream limits\n") 4412 4413 if stream: 4414 for key, values in sorted(stream.items()): 4415 info.append("\n* Max stream connections: {}\n".format(key)) 4416 4417 for value in values: 4418 info.append(" - {}\n".format(value)) 4419 4420 else: 4421 info.append("\nNot available\n") 4422 4423 infoText = "".join(info) 4424 4425 uLogger.info(infoText) 4426 4427 if self.userInfoFile: 4428 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4429 fH.write(infoText) 4430 4431 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4432 4433 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4436class Args: 4437 """ 4438 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4439 """ 4440 def __init__(self, **kwargs): 4441 self.__dict__.update(kwargs) 4442 4443 def __getattr__(self, item): 4444 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4447def ParseArgs(): 4448 """This function get and parse command line keys.""" 4449 parser = ArgumentParser() # command-line string parser 4450 4451 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4452 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4453 4454 # --- options: 4455 4456 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4457 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4458 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4459 4460 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4461 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4462 4463 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4464 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4465 4466 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4467 4468 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4469 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4470 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4471 4472 parser.add_argument("--debug-level", "--log-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4473 parser.add_argument("--more", "--more-debug", action="store_true", default=False, help="Option: `--debug-level` key only switch log level verbosity, but in addition `--more` key enable all debug information, such as net request and response headers in all methods.") 4474 4475 # --- commands: 4476 4477 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4478 4479 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4480 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4481 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4482 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4483 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4484 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4485 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4486 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4487 4488 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4489 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4490 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4491 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4492 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4493 parser.add_argument("--overview-calendar", action="store_true", help="Action: shows only the bonds calendar section (if these present in portfolio). Also, you can define `--output` key to save this information to file, default: `overview-calendar.md`.") 4494 4495 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4496 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4497 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4498 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4499 4500 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4501 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4502 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4503 4504 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4505 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4506 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4507 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4508 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4509 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4510 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4511 4512 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4513 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4514 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` (high priority) or `--figi` keys, including for currencies tickers.") 4515 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers or FIGIs, including for currencies tickers or FIGIs.") 4516 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4517 4518 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4519 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4520 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4521 4522 cmdArgs = parser.parse_args() 4523 return cmdArgs
This function get and parse command line keys.
4526def Main(**kwargs): 4527 """ 4528 Main function for work with TKSBrokerAPI in the console. 4529 4530 See examples: 4531 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4532 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4533 """ 4534 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4535 4536 if args.debug_level: 4537 uLogger.level = 10 # always debug level by default 4538 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4539 4540 exitCode = 0 4541 start = datetime.now(tzutc()) 4542 uLogger.debug("=-" * 50) 4543 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4544 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4545 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4546 )) 4547 4548 # trying to calculate full current version: 4549 buildVersion = __version__ 4550 try: 4551 v = version("tksbrokerapi") 4552 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4553 4554 except Exception: 4555 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4556 4557 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4558 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4559 4560 try: 4561 if args.version: 4562 print("TKSBrokerAPI {}".format(buildVersion)) 4563 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4564 4565 else: 4566 # Init class for trading with Tinkoff Broker: 4567 trader = TinkoffBrokerServer( 4568 token=args.token, 4569 accountId=args.account_id, 4570 useCache=not args.no_cache, 4571 ) 4572 4573 # --- set some options: 4574 4575 if args.more: 4576 trader.moreDebug = True 4577 uLogger.warning("More debug info mode is enabled! See network requests, responses and its headers in the full log or run TKSBrokerAPI platform with the `--verbosity 10` to show theres in console.") 4578 4579 if args.ticker: 4580 if args.ticker in trader.aliasesKeys: 4581 trader.ticker = trader.aliases[args.ticker] # Replace some tickers with its aliases 4582 4583 else: 4584 trader.ticker = args.ticker 4585 4586 if args.figi: 4587 trader.figi = args.figi 4588 4589 if args.depth is not None: 4590 trader.depth = args.depth 4591 4592 # --- do one command: 4593 4594 if args.list: 4595 if args.output is not None: 4596 trader.instrumentsFile = args.output 4597 4598 trader.ShowInstrumentsInfo(show=True) 4599 4600 elif args.list_xlsx: 4601 trader.DumpInstrumentsAsXLSX(forceUpdate=False) 4602 4603 elif args.bonds_xlsx is not None: 4604 if args.output is not None: 4605 trader.bondsXLSXFile = args.output 4606 4607 if len(args.bonds_xlsx) == 0: 4608 trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4609 4610 else: 4611 trader.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4612 4613 elif args.search: 4614 if args.output is not None: 4615 trader.searchResultsFile = args.output 4616 4617 trader.SearchInstruments(pattern=args.search[0], show=True) 4618 4619 elif args.info: 4620 if not (args.ticker or args.figi): 4621 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4622 raise Exception("Ticker or FIGI required") 4623 4624 if args.output is not None: 4625 trader.infoFile = args.output 4626 4627 if args.ticker: 4628 trader.SearchByTicker(requestPrice=True, show=True) # show info and current prices by ticker name 4629 4630 else: 4631 trader.SearchByFIGI(requestPrice=True, show=True) # show info and current prices by FIGI id 4632 4633 elif args.calendar is not None: 4634 if args.output is not None: 4635 trader.calendarFile = args.output 4636 4637 if len(args.calendar) == 0: 4638 bondsData = trader.ExtendBondsData(instruments=trader.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4639 4640 else: 4641 bondsData = trader.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4642 4643 trader.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4644 4645 elif args.price: 4646 if not (args.ticker or args.figi): 4647 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4648 raise Exception("Ticker or FIGI required") 4649 4650 trader.GetCurrentPrices(show=True) 4651 4652 elif args.prices is not None: 4653 if args.output is not None: 4654 trader.pricesFile = args.output 4655 4656 trader.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4657 4658 elif args.overview: 4659 if args.output is not None: 4660 trader.overviewFile = args.output 4661 4662 trader.Overview(show=True, details="full") 4663 4664 elif args.overview_digest: 4665 if args.output is not None: 4666 trader.overviewDigestFile = args.output 4667 4668 trader.Overview(show=True, details="digest") 4669 4670 elif args.overview_positions: 4671 if args.output is not None: 4672 trader.overviewPositionsFile = args.output 4673 4674 trader.Overview(show=True, details="positions") 4675 4676 elif args.overview_orders: 4677 if args.output is not None: 4678 trader.overviewOrdersFile = args.output 4679 4680 trader.Overview(show=True, details="orders") 4681 4682 elif args.overview_analytics: 4683 if args.output is not None: 4684 trader.overviewAnalyticsFile = args.output 4685 4686 trader.Overview(show=True, details="analytics") 4687 4688 elif args.overview_calendar: 4689 if args.output is not None: 4690 trader.overviewAnalyticsFile = args.output 4691 4692 trader.Overview(show=True, details="calendar") 4693 4694 elif args.deals is not None: 4695 if args.output is not None: 4696 trader.reportFile = args.output 4697 4698 if 0 <= len(args.deals) < 3: 4699 trader.Deals( 4700 start=args.deals[0] if len(args.deals) >= 1 else None, 4701 end=args.deals[1] if len(args.deals) == 2 else None, 4702 show=True, # Always show deals report in console 4703 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4704 ) 4705 4706 else: 4707 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4708 raise Exception("Incorrect value") 4709 4710 elif args.history is not None: 4711 if args.output is not None: 4712 trader.historyFile = args.output 4713 4714 if 0 <= len(args.history) < 3: 4715 dataReceived = trader.History( 4716 start=args.history[0] if len(args.history) >= 1 else None, 4717 end=args.history[1] if len(args.history) == 2 else None, 4718 interval="hour" if args.interval is None or not args.interval else args.interval, 4719 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4720 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4721 show=True, # shows all downloaded candles in console 4722 ) 4723 4724 if args.render_chart is not None and dataReceived is not None: 4725 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4726 4727 trader.ShowHistoryChart( 4728 candles=dataReceived, 4729 interact=iChart, 4730 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4731 ) 4732 4733 else: 4734 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4735 raise Exception("Incorrect value") 4736 4737 elif args.load_history is not None: 4738 histData = trader.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4739 4740 if args.render_chart is not None and histData is not None: 4741 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4742 trader.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4743 4744 trader.ShowHistoryChart( 4745 candles=histData, 4746 interact=iChart, 4747 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4748 ) 4749 4750 elif args.trade is not None: 4751 if 1 <= len(args.trade) <= 5: 4752 trader.Trade( 4753 operation=args.trade[0], 4754 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4755 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4756 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4757 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4758 ) 4759 4760 else: 4761 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4762 4763 elif args.buy is not None: 4764 if 0 <= len(args.buy) <= 4: 4765 trader.Buy( 4766 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4767 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4768 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4769 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4770 ) 4771 4772 else: 4773 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4774 4775 elif args.sell is not None: 4776 if 0 <= len(args.sell) <= 4: 4777 trader.Sell( 4778 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4779 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4780 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4781 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4782 ) 4783 4784 else: 4785 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4786 4787 elif args.order: 4788 if 4 <= len(args.order) <= 7: 4789 trader.Order( 4790 operation=args.order[0], 4791 orderType=args.order[1], 4792 lots=int(args.order[2]), 4793 targetPrice=float(args.order[3]), 4794 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4795 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4796 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4797 ) 4798 4799 else: 4800 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4801 4802 elif args.buy_limit: 4803 trader.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4804 4805 elif args.sell_limit: 4806 trader.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4807 4808 elif args.buy_stop: 4809 if 2 <= len(args.buy_stop) <= 7: 4810 trader.BuyStop( 4811 lots=int(args.buy_stop[0]), 4812 targetPrice=float(args.buy_stop[1]), 4813 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4814 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4815 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4816 ) 4817 4818 else: 4819 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4820 4821 elif args.sell_stop: 4822 if 2 <= len(args.sell_stop) <= 7: 4823 trader.SellStop( 4824 lots=int(args.sell_stop[0]), 4825 targetPrice=float(args.sell_stop[1]), 4826 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4827 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4828 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4829 ) 4830 4831 else: 4832 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4833 4834 # elif args.buy_order_grid is not None: 4835 # # update order grid work with api v2 4836 # if len(args.buy_order_grid) == 2: 4837 # orderParams = trader.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4838 # 4839 # for order in orderParams: 4840 # trader.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4841 # 4842 # else: 4843 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4844 # 4845 # elif args.sell_order_grid is not None: 4846 # # update order grid work with api v2 4847 # if len(args.sell_order_grid) >= 2: 4848 # orderParams = trader.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4849 # 4850 # for order in orderParams: 4851 # trader.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4852 # 4853 # else: 4854 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4855 4856 elif args.close_order is not None: 4857 trader.CloseOrders(args.close_order) # close only one order 4858 4859 elif args.close_orders is not None: 4860 trader.CloseOrders(args.close_orders) # close list of orders 4861 4862 elif args.close_trade: 4863 if not (args.ticker or args.figi): 4864 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4865 raise Exception("Ticker or FIGI required") 4866 4867 if args.ticker: 4868 trader.CloseTrades([args.ticker]) # close only one trade by ticker (priority) 4869 4870 else: 4871 trader.CloseTrades([args.figi]) # close only one trade by FIGI 4872 4873 elif args.close_trades is not None: 4874 trader.CloseTrades(args.close_trades) # close trades for list of tickers 4875 4876 elif args.close_all is not None: 4877 trader.CloseAll(*args.close_all) 4878 4879 elif args.limits: 4880 if args.output is not None: 4881 trader.withdrawalLimitsFile = args.output 4882 4883 trader.OverviewLimits(show=True) 4884 4885 elif args.user_info: 4886 if args.output is not None: 4887 trader.userInfoFile = args.output 4888 4889 trader.OverviewUserInfo(show=True) 4890 4891 elif args.account: 4892 if args.output is not None: 4893 trader.userAccountsFile = args.output 4894 4895 trader.OverviewAccounts(show=True) 4896 4897 else: 4898 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4899 raise Exception("There is no command to execute") 4900 4901 except Exception: 4902 trace = tb.format_exc() 4903 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4904 if e in trace: 4905 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4906 break 4907 4908 uLogger.debug(trace) 4909 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4910 exitCode = 255 # an error occurred, must be open a ticket for this issue 4911 4912 finally: 4913 finish = datetime.now(tzutc()) 4914 4915 if exitCode == 0: 4916 if args.more: 4917 uLogger.debug("All operations were finished success (summary code is 0).") 4918 4919 else: 4920 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4921 os.path.abspath(uLog.defaultLogFile), exitCode, 4922 )) 4923 4924 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4925 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4926 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4927 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4928 )) 4929 uLogger.debug("=-" * 50) 4930 4931 if not kwargs: 4932 sys.exit(exitCode) 4933 4934 else: 4935 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: